#include "pch.h"
#include "log.h"
#include "canvas.h"
#include "app.h"
#include "texture.h"
#include "node_progress_bar.h"
#include <thread>

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


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
};
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;
    
    // save viewport and clear color states
    GLint vp[4];
    GLfloat cc[4];
    glGetIntegerv(GL_VIEWPORT, vp);
    glGetFloatv(GL_COLOR_CLEAR_VALUE, cc);
    GLboolean blend = glIsEnabled(GL_BLEND);
    
    // prepare common states
    glViewport(0, 0, m_width, m_height);
    glEnable(GL_BLEND);
    
    int i = plane;
    m_tmp[i].bindFramebuffer();
    m_tmp[i].clear({ 1, 1, 1, 1 });

    ShaderManager::use(kShader::TextureAlpha);
    ShaderManager::u_int(kShaderUniform::Highlight, false);
    ShaderManager::u_int(kShaderUniform::Tex, 0);
    ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f));
    glActiveTexture(GL_TEXTURE0);
    m_sampler.bind(0);
    for (auto layer_index : m_order)
    {
        if (!m_layers[layer_index]->m_visible || m_layers[layer_index]->m_opacity == 0.f)
            continue;
        ShaderManager::u_float(kShaderUniform::Alpha, m_layers[layer_index]->m_opacity);
        m_layers[layer_index]->m_rtt[i].bindTexture();
        m_plane.draw_fill();
        m_layers[layer_index]->m_rtt[i].unbindTexture();
    }
    m_sampler.unbind();

    if (!m_pick_data[plane])
        m_pick_data[plane] = std::make_unique<glm::u8vec4[]>(m_width*m_height);
    glReadPixels(0, 0, m_width, m_height, GL_RGBA, GL_UNSIGNED_BYTE, m_pick_data[plane].get());
    m_tmp[i].unbindFramebuffer();
    
    // restore viewport and clear color states
    blend ? glEnable(GL_BLEND) : glDisable(GL_BLEND);
    glViewport(vp[0], vp[1], vp[2], vp[3]);
    glClearColor(cc[0], cc[1], cc[2], cc[3]);
    glActiveTexture(GL_TEXTURE0);

    
    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}*/)
{
    snap_history({ 0, 1, 2, 3, 4, 5 });
    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::snap_history(const std::vector<int>& planes)
{
    auto action = new ActionStroke;
    action->was_saved = !m_unsaved;
    for (auto i : planes)
    {
        if (!m_layers[m_current_layer_idx]->m_dirty_face[i])
            continue; // no stroke on this face, skip it

        m_layers[m_current_layer_idx]->m_rtt[i].bindFramebuffer();

        // save image before commit
        glm::vec2 box_or = xy(m_layers[m_current_layer_idx]->m_dirty_box[i]);
        glm::vec2 box_sz = zw(m_layers[m_current_layer_idx]->m_dirty_box[i]) - xy(m_layers[m_current_layer_idx]->m_dirty_box[i]);
        if (box_sz.x > 0 && box_sz.y > 0 && box_sz.x <= m_layers[m_current_layer_idx]->w && box_sz.y <= m_layers[m_current_layer_idx]->h)
        {
            action->m_image[i] = std::make_unique<uint8_t[]>(box_sz.x * box_sz.y * 4);
            glReadPixels(box_or.x, box_or.y, box_sz.x, box_sz.y, GL_RGBA, GL_UNSIGNED_BYTE, action->m_image[i].get());
        }
        else
        {
            LOG("snap_history invalid box size (%d, %d)", (int)box_sz.x, (int)box_sz.y);
        }

        action->m_box[i] = m_layers[m_current_layer_idx]->m_dirty_box[i];
        action->m_old_box[i] = m_layers[m_current_layer_idx]->m_dirty_box[i];
        action->m_old_dirty[i] = m_layers[m_current_layer_idx]->m_dirty_face[i];

        m_layers[m_current_layer_idx]->m_rtt[i].unbindFramebuffer();
    }
    // save history
    action->m_layer_idx = m_current_layer_idx;
    action->m_canvas = this;
    //action->m_stroke = std::move(m_current_stroke);
    action->clear_layer = true;
    ActionManager::add(action);
}
void Canvas::stroke_end()
{
    if (!m_current_stroke)
        return;
    if (m_current_stroke->has_sample())
    {
        m_commit_delayed = true;
    }
    else
    {
        stroke_commit();
        m_current_stroke = nullptr;
        m_show_tmp = false;
    }
}
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();

    m_mixer.bindFramebuffer();

    glViewport(0, 0, m_mixer.getWidth(), m_mixer.getHeight());
    glDisable(GL_DEPTH_TEST);
    glEnable(GL_SCISSOR_TEST);
    glDisable(GL_BLEND);

    glScissor(bb_min.x, bb_min.y, bb_sz.x, bb_sz.y);

    auto layer_index = m_current_layer_idx;
    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]->m_dirty_face[plane_index])
            continue;

        glm::mat4 proj = glm::perspective(glm::radians(m_cam_fov), (float)m_mixer.getWidth() / m_mixer.getHeight(), 0.1f, 1000.f);
        auto plane_mvp_z = 
            glm::scale(glm::vec3(1, -1, 1)) *
            m_proj * m_mv *
            m_plane_transform[plane_index] *
            glm::translate(glm::vec3(0, 0, -1));

        m_sampler.bind(0);
        m_sampler.bind(1);
        m_sampler.bind(2);
        const auto& b = m_current_stroke->m_brush;
        ShaderManager::use(kShader::CompDraw);
        ShaderManager::u_int(kShaderUniform::Tex, 0);
        //ShaderManager::u_int(kShaderUniform::TexA, 0);
        ShaderManager::u_int(kShaderUniform::TexStroke, 1);
        ShaderManager::u_int(kShaderUniform::TexMask, 2);
        ShaderManager::u_vec2(kShaderUniform::Resolution, m_size);
        ShaderManager::u_int(kShaderUniform::TexPattern, 3);
        ShaderManager::u_float(kShaderUniform::Alpha, 1);
        ShaderManager::u_int(kShaderUniform::Lock, false/*m_layers[layer_index]->m_alpha_locked*/);
        ShaderManager::u_int(kShaderUniform::Mask, false/*m_smask_active*/);
        ShaderManager::u_int(kShaderUniform::UseFragcoord, false);
        ShaderManager::u_int(kShaderUniform::UseDual, false);
        ShaderManager::u_int(kShaderUniform::UsePattern, false);
        ShaderManager::u_int(kShaderUniform::BlendMode, b->m_blend_mode);
        ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp_z);
        glActiveTexture(GL_TEXTURE0);
        m_layers[layer_index]->m_rtt[plane_index].bindTexture();
        glActiveTexture(GL_TEXTURE1);
        m_tmp[plane_index].bindTexture();
        glActiveTexture(GL_TEXTURE2);
        m_smask.m_rtt[plane_index].bindTexture();
        m_node->m_face_plane.draw_fill();
        m_smask.m_rtt[plane_index].unbindTexture();
        glActiveTexture(GL_TEXTURE1);
        m_tmp[plane_index].unbindTexture();
        glActiveTexture(GL_TEXTURE0);
        m_layers[layer_index]->m_rtt[plane_index].unbindTexture();
    }
    m_sampler.unbind();
    m_mixer.unbindFramebuffer();
    
    gl.restore();
}

std::array<std::vector<vertex_t>, 6> Canvas::stroke_draw_project(std::array<vertex_t, 4>& B) 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++)
    {
        auto P = poly_intersect(B.data(), B.data() + 4, m_plane_shape[i]);
        glm::mat4 plane_camera = glm::lookAt(m_plane_origin[i], m_plane_normal[i], m_plane_tangent[i]);
        int intersections = 0;
        for (int j = 0; j < P.size(); j++)
        {
            glm::vec3 ray_origin, ray_dir;
            //if (s.pos.z == 0)
            {
                //point_unproject(P[j].pos, { 0, 0, zw(m_box) }, m_mv, m_proj, ray_origin, ray_dir);

                auto clip_space = glm::vec2(P[j].pos.x, unp_vp.y - P[j].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);
            }
            //else
            //{
            //    auto m = glm::inverse(glm::lookAt({ 0, 0, 0 }, s.pos, { 0, 1, 0 }));
            //    glm::vec3 off_3d = m * glm::vec4(off[j], 0, 1);
            //    ray_origin = glm::vec3(0);
            //    ray_dir = s.pos + off_3d;
            //}

            glm::vec3 hit;
            float hit_t;
            if (ray_intersect(ray_origin, ray_dir, 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);
                P[j].pos.x = -(plane_local.x * 0.5f - 0.5f) * m_width;
                P[j].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 = m_mv * glm::vec4(hit, 1);
                P[j].pos.z = 0;
                P[j].pos.w = hit_cam.z;
                P[j].uvs *= hit_cam.z;
                P[j].uvs2 *= hit_cam.z;
                intersections++;
            }
            else
            {
                break;
            }
        }
        if (intersections >= 3)
            ret[i] = P;
    }
    return ret;
}

glm::vec4 Canvas::stroke_draw_samples(int i, std::vector<vertex_t>& P)
{
    if (!ShaderManager::ext_framebuffer_fetch)
    {
        glActiveTexture(GL_TEXTURE1);
        m_tex[i].bind(); // bg, copy of framebuffer (copied before drawing)
    }

    glm::vec2 bb_min(m_width, m_height);
    glm::vec2 bb_max(0, 0);
    for (int j = 0; j < P.size(); j++)
    {
        bb_min = glm::max({ 0, 0 }, glm::min(bb_min, xy(P[j].pos)));
        bb_max = glm::min({ m_width, m_height }, glm::max(bb_max, xy(P[j].pos)));
    }
    auto bb_sz = bb_max - bb_min;

    glm::vec2 pad(1);
    glm::ivec2 tex_pos = glm::clamp(glm::floor(bb_min) - pad, { 0, 0 }, { m_width, m_height });
    glm::ivec2 tex_sz = glm::clamp(glm::ceil(bb_sz) + pad * 2.f, { 0, 0 }, (glm::vec2)(glm::ivec2(m_width, m_height) - tex_pos));
    if (!ShaderManager::ext_framebuffer_fetch)
    {
        glCopyTexSubImage2D(GL_TEXTURE_2D, 0, tex_pos.x, tex_pos.y,
            tex_pos.x, tex_pos.y, tex_sz.x, tex_sz.y);
    }

    if (P.size() == 4)
    {
        static vertex_t rect[6];
        rect[0] = P[0];
        rect[1] = P[1];
        rect[2] = P[2];
        rect[3] = P[0];
        rect[4] = P[2];
        rect[5] = P[3];
        m_brush_shape.update_vertices(rect, 6);
    }
    else if (P.size() == 3)
    {
        m_brush_shape.update_vertices(P.data(), P.size());
    }
    else
    {
        P = triangulate_simple(P);
        m_brush_shape.update_vertices(P.data(), P.size());
    }
    m_brush_shape.draw_fill();

    if (!ShaderManager::ext_framebuffer_fetch)
    {
        glActiveTexture(GL_TEXTURE1);
        m_tex[i].unbind();
    }

    return glm::vec4(tex_pos, tex_pos + tex_sz);
}

std::vector<Canvas::StrokeFrame> Canvas::stroke_draw_compute(Stroke& stroke) const
{
    std::vector<StrokeFrame> ret;
    StrokeSample prev = stroke.m_prev_sample;
    auto samples = stroke.compute_samples();
    std::array<vertex_t, 4> B = {
        vertex_t{ {0, 0, 1, 1}, {0, 0}, {0, 0} },
        vertex_t{ {0, 0, 1, 1}, {0, 1}, {0, 1} },
        vertex_t{ {0, 0, 1, 1}, {1, 1}, {1, 1} },
        vertex_t{ {0, 0, 1, 1}, {1, 0}, {1, 0} },
    };
    for (const auto& s : samples)
    {
        if (!s.valid())
            continue;

        ret.emplace_back();
        auto& f = ret.back();

        glm::vec2 dx_mix(prev.size * 0.5f, 0), dy_mix(0, prev.size * 0.5f);
        glm::vec2 off_mix[4] = {
            -dx_mix - dy_mix, // A - bottom-left
            -dx_mix + dy_mix, // B - top-left
            +dx_mix + dy_mix, // C - top-right
            +dx_mix - dy_mix, // D - bottom-right
        };
        // P is the initial square centered at the cursor location 
        glm::vec2 dx(s.size * 0.5f, 0), dy(0, s.size * 0.5f);
        glm::vec2 off[4] = {
            -dx - dy, // A - bottom-left
            -dx + dy, // B - top-left
            +dx + dy, // C - top-right
            +dx - dy, // D - bottom-right
        };

        glm::vec2 mixer_sz(m_mixer.getWidth(), m_mixer.getHeight());
        glm::vec2 mixer_bb_min(mixer_sz);
        glm::vec2 mixer_bb_max(0, 0);
        for (int j = 0; j < 4; j++)
        {
            auto p = (xy(prev.pos) + App::I.zoom * s.scale * off_mix[j] * glm::orientate2(-s.angle));
            mixer_bb_min = glm::max({ 0, 0 }, glm::min(mixer_bb_min, p));
            mixer_bb_max = glm::min(mixer_sz, glm::max(mixer_bb_max, p));

            B[j].pos = glm::vec4(xy(s.pos) + App::I.zoom * s.scale * off[j] * glm::orientate2(-s.angle) - glm::vec2(0, 1), 1, 1);
            B[j].uvs2 = p / mixer_sz;
        }
        
        f.m_mixer_rect = { glm::floor(mixer_bb_min), glm::ceil(mixer_bb_max - mixer_bb_min) };
        f.col = glm::vec4(s.col, 1);
        f.flow = s.flow;
        f.opacity = s.opacity;
        f.shapes = stroke_draw_project(B);

        prev = s;
    }
    return ret;
}

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

    m_dirty = true;

    GLint vp[4];
    GLfloat cc[4];
    glGetIntegerv(GL_VIEWPORT, vp);
    glGetFloatv(GL_COLOR_CLEAR_VALUE, cc);
    
    const auto& brush = m_current_stroke->m_brush;
    const auto& dual_brush = m_dual_stroke->m_brush;
    auto ortho_proj = glm::ortho(0.f, (float)m_width, 0.f, (float)m_height, -1.f, 1.f);

    glViewport(0, 0, m_width, m_height);

    m_sampler_brush.bind(0);
    m_sampler_bg.bind(1);
    m_sampler_stencil.bind(2);
    m_sampler.bind(3);
    //m_sampler_linear.bind(5);

    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;

    glDisable(GL_BLEND);
    ShaderManager::use(kShader::Stroke);
    ShaderManager::u_int(kShaderUniform::Tex, 0); // brush
    if (!ShaderManager::ext_framebuffer_fetch)
        ShaderManager::u_int(kShaderUniform::TexBG, 1); // bg
    ShaderManager::u_int(kShaderUniform::TexPattern, 2); // pattern
    ShaderManager::u_int(kShaderUniform::TexMix, 3); // mixer
    //ShaderManager::u_int(kShaderUniform::TexMixA, 4); // mixer
    ShaderManager::u_float(kShaderUniform::Opacity, brush->m_tip_opacity);
    ShaderManager::u_vec2(kShaderUniform::Resolution, { m_width, m_height });
    ShaderManager::u_vec2(kShaderUniform::PatternScale, patt_scale);
    ShaderManager::u_float(kShaderUniform::PatternInvert, brush->m_pattern_invert);
    ShaderManager::u_float(kShaderUniform::PatternBright, brush->m_pattern_brightness);
    ShaderManager::u_float(kShaderUniform::PatternContrast, brush->m_pattern_contrast);
    ShaderManager::u_float(kShaderUniform::PatternDepth, brush->m_pattern_depth);
    ShaderManager::u_int(kShaderUniform::PatternBlendMode, brush->m_pattern_blend_mode);
    ShaderManager::u_vec2(kShaderUniform::PatternOffset, m_pattern_offset);
    ShaderManager::u_int(kShaderUniform::UsePattern, brush->m_pattern_enabled && brush->m_pattern_eachsample);
    ShaderManager::u_float(kShaderUniform::MixAlpha, brush->m_tip_mix);
    ShaderManager::u_float(kShaderUniform::Wet, brush->m_tip_wet);
    ShaderManager::u_float(kShaderUniform::Noise, brush->m_tip_noise);
    ShaderManager::u_mat4(kShaderUniform::MVP, ortho_proj);

    // DRAW MAIN BRUSH

    glActiveTexture(GL_TEXTURE0);
    brush->m_tip_texture->bind();
    glActiveTexture(GL_TEXTURE2);
    brush->m_pattern_texture ?
        brush->m_pattern_texture->bind() :
        glBindTexture(GL_TEXTURE_2D, 0);
    glActiveTexture(GL_TEXTURE3);
    m_mixer.bindTexture();
    auto frames = stroke_draw_compute(*m_current_stroke);
    for (auto& f : frames)
    {
        if (brush->m_tip_mix > 0.f)
        {
            stroke_draw_mix(xy(f.m_mixer_rect), zw(f.m_mixer_rect));
        }

        ShaderManager::use(kShader::Stroke);
        ShaderManager::u_vec4(kShaderUniform::Col, f.col);
        ShaderManager::u_float(kShaderUniform::Alpha, f.flow);
        ShaderManager::u_float(kShaderUniform::Opacity, f.opacity);
        for (int i = 0; i < 6; i++)
        {
            auto& P = f.shapes[i];
            if (P.size() < 3)
                continue;
            m_dirty_face[i] = true;
            m_tmp[i].bindFramebuffer();
            auto rect = stroke_draw_samples(i, P);
            m_tmp[i].unbindFramebuffer();
            m_dirty_box[i] = glm::clamp(box_union(m_dirty_box[i], rect), glm::vec4(0), glm::vec4(m_width));
        }
    }
    glActiveTexture(GL_TEXTURE3);
    m_mixer.unbindTexture();
    glActiveTexture(GL_TEXTURE0);
    brush->m_tip_texture->unbind();

    // DRAW DUAL BRUSH

    if (brush->m_dual_enabled)
    {
        ShaderManager::u_int(kShaderUniform::UsePattern, false);
        ShaderManager::u_float(kShaderUniform::MixAlpha, 0);
        ShaderManager::u_float(kShaderUniform::Wet, 0);
        ShaderManager::u_float(kShaderUniform::Noise, 0);
        
        glActiveTexture(GL_TEXTURE0);
        dual_brush->m_tip_texture ? 
            dual_brush->m_tip_texture->bind() :
            glBindTexture(GL_TEXTURE_2D, 0);
        auto frames_dual = stroke_draw_compute(*m_dual_stroke);
        for (auto& f : frames_dual)
        {
            ShaderManager::use(kShader::Stroke);
            ShaderManager::u_vec4(kShaderUniform::Col, f.col);
            ShaderManager::u_float(kShaderUniform::Alpha, f.flow);
            ShaderManager::u_float(kShaderUniform::Opacity, f.opacity);
            for (int i = 0; i < 6; i++)
            {
                auto& P = f.shapes[i];
                if (P.size() < 3)
                    continue;
                m_tmp_dual[i].bindFramebuffer();
                auto rect = stroke_draw_samples(i, P);
                m_tmp_dual[i].unbindFramebuffer();

                // this mode overflows the main brush boundries
                if (brush->m_dual_blend_mode == 0)
                    m_dirty_box[i] = glm::clamp(box_union(m_dirty_box[i], rect), glm::vec4(0), glm::vec4(m_width));
            }
        }
    }

    m_sampler_brush.unbind();
    m_sampler_bg.unbind();
    m_sampler_stencil.unbind();
    
    glViewport(vp[0], vp[1], vp[2], vp[3]);
    glClearColor(cc[0], cc[1], cc[2], cc[3]);

    if (m_commit_delayed)
    {
        stroke_commit();
        m_current_stroke = nullptr;
        m_show_tmp = false;
        m_commit_delayed = false;
    }
}
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)
{
/*
    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;
    }
*/
    point_unproject(loc, { 0, 0, zw(m_box) }, m_mv, m_proj, ray_origin, ray_dir);
    glm::vec3 hit;
    glm::vec2 fb_pos;
    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;

    m_dirty = false;
    m_dirty_stroke = true; // new stroke ready for timelapse capture
    App::I.redraw = true;
    
    // save viewport and clear color states
    GLint vp[4];
    GLfloat cc[4];
    glGetIntegerv(GL_VIEWPORT, vp);
    glGetFloatv(GL_COLOR_CLEAR_VALUE, cc);
    GLboolean blend = glIsEnabled(GL_BLEND);
    
    // allocate action to add to history
    auto action = new ActionStroke;
    action->was_saved = !m_unsaved;

    m_unsaved = true;
    App::I.title_update();

    // prepare common states
    glViewport(0, 0, m_width, m_height);
    glDisable(GL_BLEND);

    const auto& b = m_current_stroke->m_brush;

    for (int i = 0; i < 6; i++)
    {
        //m_dirty_box[i] = glm::vec4(0, 0, m_width, m_height); // reset bounding box
        if (!m_dirty_face[i])
            continue; // no stroke on this face, skip it

        m_layers[m_current_layer_idx]->m_rtt[i].bindFramebuffer();
        
        // save image before commit
        glm::vec2 box_sz = zw(m_dirty_box[i]) - xy(m_dirty_box[i]);
        action->m_image[i] = std::make_unique<uint8_t[]>(box_sz.x * box_sz.y * 4);
        glReadPixels(m_dirty_box[i].x, m_dirty_box[i].y, box_sz.x, box_sz.y, GL_RGBA, GL_UNSIGNED_BYTE, action->m_image[i].get());
        
        action->m_box[i] = m_dirty_box[i];
        action->m_old_box[i] = m_layers[m_current_layer_idx]->m_dirty_box[i];
        action->m_old_dirty[i] = m_layers[m_current_layer_idx]->m_dirty_face[i];

        auto& lbox = m_layers[m_current_layer_idx]->m_dirty_box[i];
        lbox = glm::vec4(
            glm::min(xy(m_dirty_box[i]), xy(lbox)),
            glm::max(zw(m_dirty_box[i]), zw(lbox))
            );
        m_layers[m_current_layer_idx]->m_dirty_face[i] = true;

        // copy to tmp2 for layer blending
        glActiveTexture(GL_TEXTURE0);
        m_tex2[i].bind();
        glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, m_width, m_height);
        m_tex2[i].unbind();
        
        m_tmp[i].bindTexture();
        glActiveTexture(GL_TEXTURE1);
        m_tex2[i].bind();
        m_sampler.bind(0);
        m_sampler_bg.bind(1);
        m_sampler_mask.bind(2);
        m_sampler.bind(3);
        m_sampler_stencil.bind(4);
        if (m_current_mode == kCanvasMode::Erase)
        {
            ShaderManager::use(kShader::CompErase);
            ShaderManager::u_int(kShaderUniform::Tex, 0);
            ShaderManager::u_int(kShaderUniform::TexStroke, 1);
            ShaderManager::u_int(kShaderUniform::TexMask, 2);
            ShaderManager::u_int(kShaderUniform::Mask, m_smask_active);
            ShaderManager::u_float(kShaderUniform::Alpha, 1);
            ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f));

            glActiveTexture(GL_TEXTURE0);
            m_tex2[i].bind();
            glActiveTexture(GL_TEXTURE1);
            m_tmp[i].bindTexture();
            glActiveTexture(GL_TEXTURE2);
            m_smask.m_rtt[i].bindTexture();
            m_plane.draw_fill();
            m_smask.m_rtt[i].unbindTexture();
            glActiveTexture(GL_TEXTURE1);
            m_tmp[i].unbindTexture();
            glActiveTexture(GL_TEXTURE0);
            m_tex2[i].unbind();
        }
        else
        {
            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;
            
            ShaderManager::use(kShader::CompDraw);
            ShaderManager::u_int(kShaderUniform::Tex, 0);
            ShaderManager::u_int(kShaderUniform::TexStroke, 1);
            ShaderManager::u_int(kShaderUniform::TexMask, 2);
            ShaderManager::u_int(kShaderUniform::TexDual, 3);
            ShaderManager::u_int(kShaderUniform::TexPattern, 4);
            ShaderManager::u_vec2(kShaderUniform::Resolution, m_size);
            ShaderManager::u_float(kShaderUniform::Alpha, 1);
            ShaderManager::u_int(kShaderUniform::Mask, m_smask_active);
            ShaderManager::u_int(kShaderUniform::UseFragcoord, false);
            ShaderManager::u_int(kShaderUniform::BlendMode, b->m_blend_mode);
            ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f));
            ShaderManager::u_int(kShaderUniform::UseDual, b->m_dual_enabled);
            ShaderManager::u_int(kShaderUniform::UsePattern, b->m_pattern_enabled && !b->m_pattern_eachsample);
            ShaderManager::u_int(kShaderUniform::DualBlendMode, b->m_dual_blend_mode);
            ShaderManager::u_vec2(kShaderUniform::PatternScale, patt_scale);
            ShaderManager::u_float(kShaderUniform::PatternInvert, b->m_pattern_invert);
            ShaderManager::u_float(kShaderUniform::PatternBright, b->m_pattern_brightness);
            ShaderManager::u_float(kShaderUniform::PatternContrast, b->m_pattern_contrast);
            ShaderManager::u_float(kShaderUniform::PatternDepth, b->m_pattern_depth);
            ShaderManager::u_int(kShaderUniform::PatternBlendMode, b->m_pattern_blend_mode);
            ShaderManager::u_vec2(kShaderUniform::PatternOffset, m_pattern_offset);
            ShaderManager::u_float(kShaderUniform::DualAlpha, b->m_dual_opacity);

            glActiveTexture(GL_TEXTURE0);
            m_tex2[i].bind();
            glActiveTexture(GL_TEXTURE1);
            m_tmp[i].bindTexture();
            glActiveTexture(GL_TEXTURE2);
            m_smask.m_rtt[i].bindTexture();
            glActiveTexture(GL_TEXTURE3);
            if (b->m_dual_enabled) 
                m_tmp_dual[i].bindTexture();
            glActiveTexture(GL_TEXTURE4);
            b->m_pattern_texture ?
                b->m_pattern_texture->bind() :
                glBindTexture(GL_TEXTURE_2D, 0);
            m_plane.draw_fill();
            glActiveTexture(GL_TEXTURE3);
            if (b->m_dual_enabled)
                m_tmp_dual[i].unbindTexture();
            glActiveTexture(GL_TEXTURE2);
            m_smask.m_rtt[i].unbindTexture();
            glActiveTexture(GL_TEXTURE1);
            m_tmp[i].unbindTexture();
            glActiveTexture(GL_TEXTURE0);
            m_tex2[i].unbind();
        }
//        else
//        {
//            ShaderManager::use(kShader::StrokeLayer);
//            ShaderManager::u_int(kShaderUniform::TexBG, 1);
//            ShaderManager::u_int(kShaderUniform::Lock, m_layers[m_current_layer_idx]->m_alpha_locked);
//            ShaderManager::u_float(kShaderUniform::Alpha, b->m_tip_opacity);
//
//            ShaderManager::u_int(kShaderUniform::Tex, 0);
//            ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f));
//            m_plane.draw_fill();
//            m_sampler.unbind();
//            m_sampler_bg.unbind();
//            m_tex2[i].unbind();
//            m_tmp[i].unbindTexture();
//        }
        
        m_layers[m_current_layer_idx]->m_rtt[i].unbindFramebuffer();
    }
    
    // restore viewport and clear color states
    blend ? glEnable(GL_BLEND) : glDisable(GL_BLEND);
    glViewport(vp[0], vp[1], vp[2], vp[3]);
    glClearColor(cc[0], cc[1], cc[2], cc[3]);
    glActiveTexture(GL_TEXTURE0);
    
    // save history
    action->m_layer_idx = m_current_layer_idx;
    action->m_canvas = this;
    //action->m_stroke = std::move(m_current_stroke);
    ActionManager::add(action);
}
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)
{
    // need to commit this now before starting a new stroke
    if (m_current_stroke && m_commit_delayed)
    {
        stroke_commit();
        m_current_stroke = nullptr;
        m_dual_stroke = nullptr;
        m_show_tmp = false;
        m_commit_delayed = false;
    }
    
    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;
    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);
    }

    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_show_tmp = true;
}
void Canvas::layer_add(std::string name)
{
    int idx = (int)m_layers.size();
    m_layers.push_back(std::make_unique<Layer>());
    m_layers.back()->create(m_width, m_height, name);
    m_order.push_back(idx);
    m_current_layer_idx = idx;
}
void Canvas::layer_remove(int idx) // m_order index
{
    int n = m_order[idx];
    for (auto& i : m_order)
        if (i > n)
            i--;
    m_layers[n]->destroy();
    m_layers.erase(m_layers.begin() + n);
    m_order.erase(m_order.begin() + idx);
    m_current_layer_idx = m_order[std::min<int>((int)m_layers.size() - 1, idx)];
}
void Canvas::layer_order(int idx, int pos) // m_order index
{
    std::swap(m_order[idx], m_order[pos]);
}
void Canvas::layer_merge(int source_idx, int dest_idx) // m_layer index
{
    m_dirty = false;

    // save viewport and clear color states
    GLint vp[4];
    GLfloat cc[4];
    glGetIntegerv(GL_VIEWPORT, vp);
    glGetFloatv(GL_COLOR_CLEAR_VALUE, cc);
    GLboolean blend = glIsEnabled(GL_BLEND);

    // allocate action to add to history
    auto action = new ActionStroke;

    // prepare common states
    glViewport(0, 0, m_width, m_height);
    glDisable(GL_BLEND);

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

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

/*
        // save image before commit
        glm::vec2 box_sz = m_dirty_box[i].zw() - m_dirty_box[i].xy();
        action->m_image[i] = std::make_unique<uint8_t[]>(box_sz.x * box_sz.y * 4);
        glReadPixels(m_dirty_box[i].x, m_dirty_box[i].y, box_sz.x, box_sz.y, GL_RGBA, GL_UNSIGNED_BYTE, action->m_image[i].get());

        action->m_box[i] = m_dirty_box[i];
        action->m_old_box[i] = m_layers[m_current_layer_idx]->m_dirty_box[i];
        action->m_old_dirty[i] = m_layers[m_current_layer_idx]->m_dirty_face[i];
*/

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

        // copy to tmp2 for layer blending
        glActiveTexture(GL_TEXTURE0);
        m_tex2[i].bind();
        glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, m_width, m_height);
        m_tex2[i].unbind();

        m_sampler.bind(0);
        m_sampler_bg.bind(1);
        {
            ShaderManager::use(kShader::CompDraw);
            ShaderManager::u_int(kShaderUniform::Tex, 0); // dest
            ShaderManager::u_int(kShaderUniform::TexStroke, 1); // source
            ShaderManager::u_vec2(kShaderUniform::Resolution, m_size);
            ShaderManager::u_float(kShaderUniform::PatternAlpha, 0);
            ShaderManager::u_float(kShaderUniform::Alpha, m_layers[source_idx]->m_opacity);
            ShaderManager::u_int(kShaderUniform::Lock, false);
            ShaderManager::u_int(kShaderUniform::UseFragcoord, false);
            ShaderManager::u_int(kShaderUniform::BlendMode, m_layers[source_idx]->m_blend_mode);
            ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f));
            ShaderManager::u_int(kShaderUniform::UseDual, false);
            ShaderManager::u_int(kShaderUniform::UsePattern, false);

            glActiveTexture(GL_TEXTURE0);
            m_tex2[i].bind();
            glActiveTexture(GL_TEXTURE1);
            m_layers[source_idx]->m_rtt[i].bindTexture();
            m_plane.draw_fill();
            m_layers[source_idx]->m_rtt[i].unbindTexture();
            glActiveTexture(GL_TEXTURE0);
            m_tex2[i].unbind();
        }

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

    // restore viewport and clear color states
    blend ? glEnable(GL_BLEND) : glDisable(GL_BLEND);
    glViewport(vp[0], vp[1], vp[2], vp[3]);
    glClearColor(cc[0], cc[1], cc[2], cc[3]);
    glActiveTexture(GL_TEXTURE0);

/*
    // save history
    action->m_layer_idx = m_current_layer_idx;
    action->m_canvas = this;
    action->m_stroke = std::move(m_current_stroke);
    ActionManager::add(action);
*/
}
void Canvas::resize(int width, int height)
{
    m_width = width;
    m_height = height;
    m_size = { width, height };
    for (int i = 0; i < 6; i++)
    {
#if defined(__IOS__) || defined(__ANDROID__)
        m_tmp[i].create(width, height, -1, GL_RGBA8);
        m_tmp_dual[i].create(width, height, -1, GL_RGBA8);
        m_tex[i].create(width, height, GL_RGBA8);
#else
        m_tmp[i].create(width, height, -1, GL_RGBA32F);
        m_tmp_dual[i].create(width, height, -1, GL_RGBA32F);
        m_tex[i].create(width, height, GL_RGBA32F);
#endif
        m_tex2[i].create(width, height, GL_RGBA8);
    }
    for (auto& l : m_layers)
        l->resize(width, height);
    m_smask.create(width, height, "mask");
    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();
}

bool Canvas::create(int width, int height)
{
    m_width = width;
    m_height = height;
    m_size = { width, height };
    for (int i = 0; i < 6; i++)
    {
#if defined(__IOS__) || defined(__ANDROID__)
        m_tmp[i].create(width, height, -1, GL_RGBA8);
        m_tmp_dual[i].create(width, height, -1, GL_RGBA8);
        m_tex[i].create(width, height, GL_RGBA8);
        m_sampler_brush.create();
#else
        m_tmp[i].create(width, height, -1, GL_RGBA32F);
        m_tmp_dual[i].create(width, height, -1, GL_RGBA32F);
        m_tex[i].create(width, height, GL_RGBA32F);
        m_sampler_brush.create(GL_LINEAR, GL_CLAMP_TO_BORDER);
#endif
        m_tex2[i].create(width, height, GL_RGBA8);
    }
    m_sampler.create(GL_NEAREST);
    m_sampler.create(GL_LINEAR);
    m_sampler_nearest.create(GL_NEAREST);
    m_sampler_brush.set_filter(GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR);
    m_sampler_brush.set_border({ 1, 1, 1, 1 });
    m_sampler_bg.create(GL_NEAREST);
    m_sampler_mask.create(GL_LINEAR);
    m_sampler_stencil.create(GL_LINEAR, GL_REPEAT);
    m_sampler_mix.create(GL_NEAREST, GL_REPEAT);
    m_sampler_linear.create();
    m_plane.create<1>(1, 1);
    m_plane_brush.create<1>(1, 1);
    m_brush_shape.create();
    m_mesh.create();
    m_brush_mix.create(8, 8);
    for (auto& l : m_layers)
        l->create(width, height, "");
    m_smask.create(width, height, "mask");
    //m_smask.clear({1, 1, 1, 1});
    m_unsaved = true;
    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)
{
#if __IOS__
    import_equirectangular_thread(file_path);
    return;
#endif
    std::thread t(&Canvas::import_equirectangular_thread, this, file_path);
    t.detach();
}

void Canvas::import_equirectangular_thread(std::string file_path)
{
    App::I.async_start();
    
    gl_state gl;
    gl.save();
    snap_history({0,1,2,3,4,5});
    m_unsaved = true;

    Image img;
    img.load_file(file_path);

    if (img.width == img.height / 6)
    {
        Texture2D tex;
        static GLint indices[] = { 5, 0, 4, 1, 2, 3 };
        static GLint formats[] = { GL_RED, GL_RG, GL_RGB, GL_RGBA };
        static GLint iformats[] = { GL_R8, GL_RG8, GL_RGB8, GL_RGBA8 };
        tex.create(img.width, img.width, iformats[img.comp - 1], formats[img.comp - 1]);
        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) {
            tex.update(img.m_data.get() + indices[i] * stride);
            m_sampler.bind(0);
            glActiveTexture(GL_TEXTURE0);
            tex.bind();
            ShaderManager::use(kShader::Texture);
            ShaderManager::u_int(kShaderUniform::Tex, 0);
            ShaderManager::u_mat4(kShaderUniform::MVP, glm::scale(glm::vec3(-1, -1, 1)));
            plane.draw_fill();
            tex.unbind();
            m_sampler.unbind();
        });
    }
    else
    {
        Texture2D tex;
        tex.load_file(file_path);
        Sphere sphere;
        glDisable(GL_DEPTH_TEST);
        sphere.create<64, 64>(2.f);
        draw_objects([&](const glm::mat4& camera, const glm::mat4& proj, int i) {
            m_sampler.bind(0);
            glActiveTexture(GL_TEXTURE0);
            tex.bind();
            ShaderManager::use(kShader::Texture);
            ShaderManager::u_int(kShaderUniform::Tex, 0);
            ShaderManager::u_mat4(kShaderUniform::MVP, proj * camera *
                glm::eulerAngleY(glm::radians(180.f)) * glm::scale(glm::vec3(1, -1, 1)));
            sphere.draw_fill();
            tex.unbind();
            m_sampler.unbind();
        });
    }
    for (int i = 0; i < 6; i++)
    {
        m_layers[m_current_layer_idx]->m_dirty_box[i] = glm::vec4(0, 0, m_width, m_height);
        m_layers[m_current_layer_idx]->m_dirty_face[i] = true;
    }
    App::I.async_update();
    gl.restore();
    App::I.async_end();
}

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

void Canvas::export_equirectangular_thread(std::string file_path)
{
    gl_state gl;

    App::I.async_start();
    
    std::shared_ptr<NodeProgressBar> pb;
    if (App::I.layout.m_loaded)
    {
        pb = std::make_shared<NodeProgressBar>();
        pb->m_manager = &App::I.layout;
        pb->init();
        pb->create();
        pb->loaded();
        pb->m_progress->SetWidthP(0);
        pb->m_title->set_text("Export Pano Image");
        App::I.layout[App::I.main_id]->add_child(pb);
        App::I.async_update();
    }

    // save viewport and clear color states
    GLint vp[4];
    GLfloat cc[4];
    glGetIntegerv(GL_VIEWPORT, vp);
    glGetFloatv(GL_COLOR_CLEAR_VALUE, cc);
    GLboolean blend = glIsEnabled(GL_BLEND);

    // prepare common states
    glViewport(0, 0, m_width, m_height);
    glEnable(GL_BLEND);

    RTT m_latlong;
    m_latlong.create(m_width * 4, m_height * 2); // NOTE: w and h must be equal to make sense

    GLuint cube_id;
    glGenTextures(1, &cube_id);
    glBindTexture(GL_TEXTURE_CUBE_MAP, cube_id);
    for (GLuint i = 0; i < 6; i++)
        glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGBA8, m_width, m_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
    static int faces[]{
        GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, // front
        GL_TEXTURE_CUBE_MAP_NEGATIVE_X, // right
        GL_TEXTURE_CUBE_MAP_POSITIVE_Z, // back
        GL_TEXTURE_CUBE_MAP_POSITIVE_X, // left
        GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, // top
        GL_TEXTURE_CUBE_MAP_POSITIVE_Y, // bottom
    };
    
    int progress = 0;
    int total = 6 + 2;

    Texture2D face;
    face.create(m_width, m_height);

    for (int i = 0; i < 6; i++)
    {
        // prepare common states
        glViewport(0, 0, m_width, m_height);
        glDisable(GL_BLEND);

        ShaderManager::use(kShader::TextureBlend);
        ShaderManager::u_int(kShaderUniform::Tex, 0);
        ShaderManager::u_int(kShaderUniform::TexA, 1);
        ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f));

        m_tmp[i].bindFramebuffer();
        // clear transparent not to mess with blending modes
        m_tmp[i].clear({ 1, 1, 1, 0 });

        if (!ShaderManager::ext_framebuffer_fetch)
        {
            ShaderManager::u_int(kShaderUniform::TexBG, 2);
            glActiveTexture(GL_TEXTURE2);
            face.bind();
            m_sampler_bg.bind(2);
        }
        m_sampler_bg.bind(0);   // nearest
        m_sampler_mask.bind(1); // linear
        for (auto layer_index : m_order)
        {
            if (!m_layers[layer_index]->m_visible ||
                m_layers[layer_index]->m_opacity == 0.f ||
                !m_layers[layer_index]->m_dirty_face[i])
                continue;
            if (!ShaderManager::ext_framebuffer_fetch)
            {
                glActiveTexture(GL_TEXTURE2);
                glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, m_width, m_height);
            }
            ShaderManager::u_int(kShaderUniform::BlendMode, m_layers[layer_index]->m_blend_mode);
            ShaderManager::u_float(kShaderUniform::Alpha, m_layers[layer_index]->m_opacity);
            glActiveTexture(GL_TEXTURE0);
            m_layers[layer_index]->m_rtt[i].bindTexture();
            glActiveTexture(GL_TEXTURE1);
            m_layers[layer_index]->m_rtt[i].bindTexture();
            m_plane.draw_fill();
            m_layers[layer_index]->m_rtt[i].unbindTexture();
            glActiveTexture(GL_TEXTURE0);
            m_layers[layer_index]->m_rtt[i].unbindTexture();
        }

        if (!ShaderManager::ext_framebuffer_fetch)
        {
            glActiveTexture(GL_TEXTURE2);
            face.unbind();
        }

        // now blend with the background
        glEnable(GL_BLEND);
        ShaderManager::use(kShader::Texture);
        ShaderManager::u_int(kShaderUniform::Tex, 0);
        ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f));
        m_sampler_mask.bind(0); // linear
        glActiveTexture(GL_TEXTURE0);
        face.bind();
        // copy the framebuffer before clearing to white
        glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, m_width, m_height);
        m_tmp[i].clear({ 1, 1, 1, 0 });
        m_plane.draw_fill();
        face.unbind();

        // copy result to cubemap
        glBindTexture(GL_TEXTURE_CUBE_MAP, cube_id);
        glCopyTexImage2D(faces[i], 0, GL_RGBA8, 0, 0, m_width, m_height, 0);
        glBindTexture(GL_TEXTURE_CUBE_MAP, 0);

        m_tmp[i].unbindFramebuffer();

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

        if (App::I.layout.m_loaded)
        {
            pb->m_progress->SetWidthP(p);
            gl.save();
            App::I.async_update();
            gl.restore();
        }
    }

    face.destroy();

    //auto data = std::make_unique<uint8_t[]>(m_tmp[0].bytes());
    //for (int i = 0; i < 1; i++)
    //{
    //    m_tmp[i].readTextureData(data.get());
    //    static char name[128];
    //    sprintf(name, "%s/Face%d.png", data_path.c_str(), i);
    //    LOG("writing %s", name);
    //    int ret = stbi_write_png(name, m_tmp[i].getWidth(), m_tmp[i].getHeight(), 4, data.get(), m_tmp[i].stride());
    //}
    
    glDisable(GL_BLEND);
    glViewport(0, 0, m_latlong.getWidth(), m_latlong.getHeight());
    glActiveTexture(GL_TEXTURE0);
    m_latlong.bindFramebuffer();
    ShaderManager::use(kShader::Equirect);
    ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f));
    ShaderManager::u_int(kShaderUniform::Tex, 0);
    glBindTexture(GL_TEXTURE_CUBE_MAP, cube_id);
    m_sampler_mask.bind(0);
    m_plane.draw_fill();
    m_sampler_mask.unbind();
    glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
    m_latlong.unbindFramebuffer();
    {
        auto latlong_data = std::make_unique<uint8_t[]>(m_latlong.bytes());
        m_latlong.readTextureData(latlong_data.get());

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

            if (App::I.layout.m_loaded)
            {
                pb->m_progress->SetWidthP(p);
                gl.save();
                App::I.async_update();
                gl.restore();
            }
        }

        LOG("writing %s", file_path.c_str());
        if (file_path.substr(file_path.size() - 4) == ".jpg")
        {
            stbi_write_jpg(file_path.c_str(), m_latlong.getWidth(), m_latlong.getHeight(), 4, latlong_data.get(), 100);
            inject_xmp(file_path);
        }
        else if (file_path.substr(file_path.size() - 4) == ".png")
        {
            stbi_write_png(file_path.c_str(), m_latlong.getWidth(), m_latlong.getHeight(), 4, latlong_data.get(), 0);
        }

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

            if (App::I.layout.m_loaded)
            {
                pb->m_progress->SetWidthP(p);
                gl.save();
                App::I.async_update();
                gl.restore();
            }
        }

        //int ret = stbi_write_png(name, m_latlong.getWidth(), m_latlong.getHeight(), 4, latlong_data.get(), m_latlong.stride());
#ifdef __IOS__
        [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
            NSURL* url = [NSURL fileURLWithPath:[NSString stringWithUTF8String:file_path.c_str()]];
            PHAssetChangeRequest *changeRequest = [PHAssetChangeRequest creationRequestForAssetFromImageAtFileURL:url];
            changeRequest.creationDate          = [NSDate date];
        } completionHandler:^(BOOL success, NSError *error) {
            if (success) {
                NSLog(@"successfully saved");
            }
            else {
                NSLog(@"error saving to photos: %@", error);
            }
        }];
#endif
    }

    glDeleteTextures(1, &cube_id);
    m_latlong.destroy();

    // restore viewport and clear color states
    blend ? glEnable(GL_BLEND) : glDisable(GL_BLEND);
    glViewport(vp[0], vp[1], vp[2], vp[3]);
    glClearColor(cc[0], cc[1], cc[2], cc[3]);
    glActiveTexture(GL_TEXTURE0);

    if (App::I.layout.m_loaded)
    {
        pb->destroy();
        App::I.async_update();
    }
    App::I.async_end();
}

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_layers(std::string file_name, std::function<void()> on_complete)
{
    if (App::I.check_license())
    {
#if __IOS__
        export_layers_thread(file_name);
        if (on_complete)
            on_complete();
        return;
#endif
        std::thread t([=] {
            export_layers_thread(file_name);
            if (on_complete)
                on_complete();
        });
        t.detach();
    }
}

void Canvas::export_layers_thread(std::string file_name)
{
    // save viewport and clear color states
    GLint vp[4];
    GLfloat cc[4];
    glGetIntegerv(GL_VIEWPORT, vp);
    glGetFloatv(GL_COLOR_CLEAR_VALUE, cc);
    GLboolean blend = glIsEnabled(GL_BLEND);

    gl_state gl;
    App::I.async_start();
    std::shared_ptr<NodeProgressBar> pb;
    if (App::I.layout.m_loaded)
    {
        pb = std::make_shared<NodeProgressBar>();
        pb->m_manager = &App::I.layout;
        pb->init();
        pb->create();
        pb->loaded();
        pb->m_progress->SetWidthP(0);
        pb->m_title->set_text("Export Pano Layers");
        App::I.layout[App::I.main_id]->add_child(pb);
        App::I.async_update();
    }
    int progress = 0;
    int total = (m_order.size() + 1) * 6;

    // prepare common states
    glViewport(0, 0, m_width, m_height);

    RTT m_latlong;
    m_latlong.create(m_width * 4, m_height * 2); // NOTE: w and h must be equal to make sense

    GLuint cube_id;
    glGenTextures(1, &cube_id);
    glBindTexture(GL_TEXTURE_CUBE_MAP, cube_id);
    for (GLuint i = 0; i < 6; i++)
        glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGBA8, m_width, m_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
    int faces[]{
        GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, // front
        GL_TEXTURE_CUBE_MAP_NEGATIVE_X, // right
        GL_TEXTURE_CUBE_MAP_POSITIVE_Z, // back
        GL_TEXTURE_CUBE_MAP_POSITIVE_X, // left
        GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, // top
        GL_TEXTURE_CUBE_MAP_POSITIVE_Y, // bottom
    };

    int seq = 0;
    for (auto layer_index : m_order)
    {
        glViewport(0, 0, m_width, m_height);
        for (int i = 0; i < 6; i++)
        {
            glEnable(GL_BLEND); 
            m_tmp[i].bindFramebuffer();
            
            //if (seq == 0)
            //{
            //    m_tmp[i].clear({ 1, 1, 1, 1 });
            //    ShaderManager::use(kShader::Checkerboard);
            //    ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f));
            //    m_plane.draw_fill();
            //    glEnable(GL_BLEND);
            //}
            //else
            {
                m_tmp[i].clear({ 1, 1, 1, 0 });
                //glDisable(GL_BLEND);
            }

            glActiveTexture(GL_TEXTURE0);
            ShaderManager::use(kShader::TextureAlpha);
            ShaderManager::u_float(kShaderUniform::Alpha, 1);
            ShaderManager::u_int(kShaderUniform::Highlight, false);
            ShaderManager::u_int(kShaderUniform::Tex, 0);
            ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f));
            
            m_sampler_mask.bind(0);
            m_layers[layer_index]->m_rtt[i].bindTexture();
            m_plane.draw_fill();
            m_layers[layer_index]->m_rtt[i].unbindTexture();
            m_sampler_mask.unbind();
            
            // copy result to cubemap
            glBindTexture(GL_TEXTURE_CUBE_MAP, cube_id);
            glCopyTexImage2D(faces[i], 0, GL_RGBA8, 0, 0, m_width, m_height, 0);
            glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
            
            m_tmp[i].unbindFramebuffer();

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

            if (App::I.layout.m_loaded)
            {
                pb->m_progress->SetWidthP(p);
                gl.save();
                App::I.async_update();
                gl.restore();
            }
        }


        glViewport(0, 0, m_latlong.getWidth(), m_latlong.getHeight());
        glActiveTexture(GL_TEXTURE0);
        m_latlong.bindFramebuffer();
        m_latlong.clear({ 1, 1, 1, 0 });
        ShaderManager::use(kShader::Equirect);
        ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f));
        ShaderManager::u_int(kShaderUniform::Tex, 0);
        glBindTexture(GL_TEXTURE_CUBE_MAP, cube_id);
        glDisable(GL_BLEND);
        m_sampler_mask.bind(0);
        m_plane.draw_fill();
        m_sampler_mask.unbind();
        glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
        m_latlong.unbindFramebuffer();
        {
            auto latlong_data = std::make_unique<uint8_t[]>(m_latlong.bytes());
            m_latlong.readTextureData(latlong_data.get());
            static char name[128];
            sprintf(name, "%s/%s-layer-%02d.png", App::I.work_path.c_str(), file_name.c_str(), seq);
            seq++;
            LOG("writing %s", name);
            App::I.async_end();
            int ret = stbi_write_png(name, m_latlong.getWidth(), m_latlong.getHeight(), 4, latlong_data.get(), m_latlong.stride());
            App::I.async_start();
        }

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

        if (App::I.layout.m_loaded)
        {
            pb->m_progress->SetWidthP(p);
            gl.save();
            App::I.async_update();
            gl.restore();
        }
    }

    glDeleteTextures(1, &cube_id);
    m_latlong.destroy();

    if (App::I.layout.m_loaded)
    {
        pb->destroy();
        App::I.async_update();
    }
    App::I.async_end();

    // restore viewport and clear color states
    blend ? glEnable(GL_BLEND) : glDisable(GL_BLEND);
    glViewport(vp[0], vp[1], vp[2], vp[3]);
    glClearColor(cc[0], cc[1], cc[2], cc[3]);
    glActiveTexture(GL_TEXTURE0);
}

void Canvas::export_cubes()
{
    if (!App::I.check_license())
        return;
#ifdef __OBJC__
    NSMutableArray* files = [NSMutableArray array];
#endif
	std::array<std::string, 6> names {
		"pz", "px", "nz", "nx",
		"py", "ny"
	};
	const int stride = m_width * 4;
	auto buffer = std::make_unique<uint8_t[]>(m_width * m_height * 4);
	auto flipped = std::make_unique<uint8_t[]>(m_width * m_height * 4);
	for (int layer = 0; layer < m_order.size(); layer++)
	{
		for (int plane = 0; plane < 6; plane++)
		{
			auto& l = m_layers[m_order[layer]];
			l->m_rtt[plane].bindFramebuffer();
			glReadPixels(0, 0, m_width, m_height, GL_RGBA, GL_UNSIGNED_BYTE, buffer.get());
			l->m_rtt[plane].unbindFramebuffer();

			if (plane < 4)
			{
				for (int y = 0; y < m_height; y++)
				{
					int y_rev = m_height - y - 1;
					std::copy_n(buffer.get() + y * stride, stride, flipped.get() + y_rev * stride);
				}
				std::swap(buffer, flipped);
			}
			else
			{
				for (int y = 0; y < m_height; y++)
				{
					auto src = (glm::u8vec4*)(buffer.get() + y * stride);
					auto dst = (glm::u8vec4*)(flipped.get() + y * stride);
					for (int x = 0; x < m_width; x++)
					{
						int x_rev = m_width - x - 1;
						dst[x_rev] = src[x];
					}
					//std::copy_backward(src + stride, src, dst + stride);
				}
				std::swap(buffer, flipped);
			}

			static char name[128];
			sprintf(name, "%s-%02d-%d.png", App::I.work_path.c_str(), layer, plane);
			int ret = stbi_write_png(name, m_width, m_height, 4, buffer.get(), 0);


#ifdef __IOS__
			[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
				NSURL* url = [NSURL fileURLWithPath : [NSString stringWithUTF8String : name]];
				PHAssetChangeRequest *changeRequest = [PHAssetChangeRequest creationRequestForAssetFromImageAtFileURL : url];
				changeRequest.creationDate = [NSDate date];
			} completionHandler: ^ (BOOL success, NSError *error) {
				if (success) {
					NSLog(@"successfully saved");
				}
				else {
					NSLog(@"error saving to photos : %@", error);
				}
			}];
#endif
#ifdef __OBJC__
            [files addObject:[NSString stringWithUTF8String : name]];
#endif
		}
	}
#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())
    {
#if __IOS__
        bool ret = project_save_thread(App::I.doc_path);
        if (on_complete)
            on_complete(ret);
        return;
#endif
        std::thread t([=] {
            bool ret = project_save_thread(App::I.doc_path);
            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())
    {
#if __IOS__
        bool ret = project_save_thread(file_path);
        if (on_complete)
            on_complete(ret);
        return;
#endif
        std::thread t([=] {
            bool ret = project_save_thread(file_path);
            if (on_complete)
                on_complete(ret);
        });
        t.detach();
    }
    else
    {
        LOG("no license, no save");
    }
}

bool Canvas::project_save_thread(std::string file_path)
{
    gl_state gl;

    // 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;
    
    auto start = file_path.rfind('/') + 1;
    std::string file_name = file_path.substr(start, file_path.length() - start - strlen(".ppi"));
    std::string tmp_path = App::I.data_path + '/' + file_name + ".tmp.ppi";
    
    LOG("file name %s", file_name.c_str());
    LOG("tmp path %s", tmp_path.c_str());

    bool use_tmp = false;
    // check if file already exists
    if ((fp = fopen(file_path.c_str(), "rb")))
    {
        fclose(fp);
        LOG("use tmp file");
        // use tmp file for writing
        use_tmp = true;
        if (!(fp = fopen(tmp_path.c_str(), "wb")))
        {
            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
    App::I.async_start();
    Image thumb = thumbnail_generate(ppi_header.thumb_header.width, ppi_header.thumb_header.height);
    auto pb = std::make_shared<NodeProgressBar>();
    pb->m_manager = &App::I.layout;
    pb->init();
    pb->create();
    pb->loaded();
    pb->m_progress->SetWidthP(0);
    pb->m_title->set_text("Saving Pano Project");
    App::I.layout[App::I.main_id]->add_child(pb);
    App::I.async_update();
    App::I.async_end();
    
    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 progress = 0;
    int total = (int)m_layers.size() * 6;
    
    for (int i = 0; i < (int)m_layers.size(); i++)
    {
        int n_order = m_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 > 1)
        {
            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);
        }
        
        App::I.async_start();
        auto snap = m_layers[i]->snapshot();
        App::I.async_update();
        App::I.async_end();
        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;
            App::I.async_start();
            pb->m_progress->SetWidthP(p);
            LOG("progress: %f", p);
            App::I.async_update();
            App::I.async_end();
        }
    }
    fclose(fp);
    bool success = false;
    if (use_tmp)
    {
        LOG("project saved tmp to %s", tmp_path.c_str());
        LOG("swapping to %s", file_path.c_str());
        if (std::remove(file_path.c_str()) == 0)
        {
            if (std::rename(tmp_path.c_str(), file_path.c_str()) == 0)
            {
                success = true;
                LOG("tmp file swapped succesfully");
            }
            else
            {
                success = false;
                LOG("tmp file NOT swapped, original removed");
            }
        }
        else
        {
            success = false;
            LOG("could not remove %s", file_path.c_str());
        }
    }
    else
    {
        success = true;
        LOG("project saved to %s", file_path.c_str());
    }

    if (success)
    {
        m_unsaved = false;
        m_newdoc = false;
    }

    App::I.async_start();
    pb->destroy();
    App::I.title_update();
    App::I.async_update();
    App::I.async_end();
    
    return success;
}

void Canvas::project_open(std::string file_path, std::function<void(bool)> on_complete)
{
#if __IOS__
    bool ret = project_open_thread(file_path);
    if (on_complete)
        on_complete(ret);
    return;
#endif
    std::thread t([=] {
        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;
    }
    
    gl_state gl;
    std::shared_ptr<NodeProgressBar> pb;
    if (App::I.layout.m_loaded)
    {
        App::I.async_start();
        pb = std::make_shared<NodeProgressBar>();
        pb->m_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);
        gl.save();
        App::I.async_update();
        gl.restore();
        App::I.async_end();
    }

    // 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);
    
    const int bytes = m_width * m_height * 4;
    Layer::Snapshot snap;
    snap.create(m_width, m_height); // allocate single data, no box should be bigger

    int progress = 0;
    int total = n_layers * 6;
    
    App::I.async_start();
    for (auto& l : m_layers)
        l->destroy();
    m_layers.clear();
    m_order.clear();
    //clear_all();
    resize(m_width, m_height);
    App::I.async_end();

    std::vector<int> tmp_order;
    std::vector<std::unique_ptr<Layer>> tmp_layers;

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

        tmp_layers.push_back(std::make_unique<Layer>());
        auto& layer = tmp_layers.back();

        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 > 1)
        {
            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);
        }

        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)
            {
                App::I.async_start();
                pb->m_progress->SetWidthP(p);
                gl.save();
                App::I.async_update();
                gl.restore();
                App::I.async_end();
            }
        }

        App::I.async_start();
        tmp_layers.back()->create(m_width, m_height, name.c_str());
        tmp_layers.back()->clear({ 0, 0, 0, 0 });
        tmp_layers.back()->restore(snap);
        tmp_order.push_back(n_order);
        App::I.async_end();
    }

    std::swap(tmp_order, m_order);
    std::swap(tmp_layers, m_layers);

    fclose(fp);
    LOG("project restore from %s", file_path.c_str());
    
    m_unsaved = false;
    m_newdoc = false;
    if (App::I.layout.m_loaded)
    {
        App::I.async_start();
        pb->destroy();
        gl.save();
        App::I.async_update();
        gl.restore();
        App::I.title_update();
        App::I.async_end();
    }
    return true;
}

Image Canvas::thumbnail_generate(int w, int h)
{
    // save viewport and clear color states
    GLint vp[4];
    GLfloat cc[4];
    glGetIntegerv(GL_VIEWPORT, vp);
    glGetFloatv(GL_COLOR_CLEAR_VALUE, cc);
    GLboolean blend = glIsEnabled(GL_BLEND);

    // prepare common states
    glViewport(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);

    // 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++)
    {
        glDisable(GL_BLEND);
        auto plane_mvp = proj * m_mv * m_plane_transform[i] * glm::translate(glm::vec3(0, 0, -1));

        ShaderManager::use(kShader::TextureBlend);
        ShaderManager::u_int(kShaderUniform::Tex, 0);
        ShaderManager::u_int(kShaderUniform::TexA, 1);
        ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp);
        if (!ShaderManager::ext_framebuffer_fetch)
        {
            ShaderManager::u_int(kShaderUniform::TexBG, 2);
            glActiveTexture(GL_TEXTURE2);
            blendtex.bind();
            m_sampler_bg.bind(2);
        }
        m_sampler_bg.bind(0);   // nearest
        m_sampler_mask.bind(1); // linear
        for (auto layer_index : m_order)
        {
            if (!m_layers[layer_index]->m_visible ||
                m_layers[layer_index]->m_opacity == 0.f ||
                !m_layers[layer_index]->m_dirty_face[i])
                continue;
            if (!ShaderManager::ext_framebuffer_fetch)
            {
                glActiveTexture(GL_TEXTURE2);
                glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, w, h);
            }
            ShaderManager::u_int(kShaderUniform::BlendMode, m_layers[layer_index]->m_blend_mode);
            ShaderManager::u_float(kShaderUniform::Alpha, m_layers[layer_index]->m_opacity);
            glActiveTexture(GL_TEXTURE0);
            m_layers[layer_index]->m_rtt[i].bindTexture();
            glActiveTexture(GL_TEXTURE1);
            m_layers[layer_index]->m_rtt[i].bindTexture();
            m_face_plane.draw_fill();
            m_layers[layer_index]->m_rtt[i].unbindTexture();
            glActiveTexture(GL_TEXTURE0);
            m_layers[layer_index]->m_rtt[i].unbindTexture();
        }

        if (!ShaderManager::ext_framebuffer_fetch)
        {
            glActiveTexture(GL_TEXTURE2);
            blendtex.unbind();
        }

        glActiveTexture(GL_TEXTURE0);
        blendtex.bind();
        // copy the content of the fb before drawing the grid
        glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, w, h);

        // draw the grid
        ShaderManager::use(kShader::Checkerboard);
        ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp);
        m_face_plane.draw_fill();

        // now blend with the background
        glEnable(GL_BLEND);
        ShaderManager::use(kShader::Texture);
        ShaderManager::u_int(kShaderUniform::Tex, 0);
        ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f));
        m_sampler_mask.bind(0); // linear
        m_plane.draw_fill();
        
        blendtex.unbind();
    }

    fb.unbindFramebuffer();

    // read the rendered image
    Image image;
    image.create(w, h);
    fb.readTextureData((uint8_t*)image.data());

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

    // restore viewport and clear color states
    blend ? glEnable(GL_BLEND) : glDisable(GL_BLEND);
    glViewport(vp[0], vp[1], vp[2], vp[3]);
    glClearColor(cc[0], cc[1], cc[2], cc[3]);
    glActiveTexture(GL_TEXTURE0);

    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)
{
    // save viewport and clear color states
    GLint vp[4];
    GLfloat cc[4];
    glGetIntegerv(GL_VIEWPORT, vp);
    glGetFloatv(GL_COLOR_CLEAR_VALUE, cc);
    GLboolean blend = glIsEnabled(GL_BLEND);

    // prepare common states
    glViewport(0, 0, layer.w, layer.h);
    glDisable(GL_BLEND);

    GLuint rboID;
    glGenRenderbuffers(1, &rboID);
    glBindRenderbuffer(GL_RENDERBUFFER, rboID);
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, layer.w, layer.h);
    glBindRenderbuffer(GL_RENDERBUFFER, 0);

    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.m_rtt[i].bindFramebuffer();
        glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rboID);

        observer(plane_camera, proj, i);

        glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, 0);
        layer.m_rtt[i].unbindFramebuffer();

        layer.m_dirty_face[i] = true;
        layer.m_dirty_box[i] = { 0, 0, layer.w, layer.h };
    }

    glDeleteRenderbuffers(1, &rboID);

    // restore viewport and clear color states
    blend ? glEnable(GL_BLEND) : glDisable(GL_BLEND);
    glViewport(vp[0], vp[1], vp[2], vp[3]);
    glClearColor(cc[0], cc[1], cc[2], cc[3]);
    glActiveTexture(GL_TEXTURE0);
}

void Canvas::draw_objects(std::function<void(const glm::mat4& camera, const glm::mat4& proj, int i)> observer, Layer& layer)
{
    // save viewport and clear color states
    GLint vp[4];
    GLfloat cc[4];
    glGetIntegerv(GL_VIEWPORT, vp);
    glGetFloatv(GL_COLOR_CLEAR_VALUE, cc);
    GLboolean blend = glIsEnabled(GL_BLEND);

    // prepare common states
    glViewport(0, 0, layer.w, layer.h);
    glDisable(GL_BLEND);

    GLuint rboID;
    glGenRenderbuffers(1, &rboID);
    glBindRenderbuffer(GL_RENDERBUFFER, rboID);
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, layer.w, layer.h);
    glBindRenderbuffer(GL_RENDERBUFFER, 0);

    RTT rtt;
    rtt.create(layer.w, layer.h);
    rtt.bindFramebuffer();
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rboID);
    rtt.unbindFramebuffer();

    // allocate action to add to history
    auto 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.m_rtt[i].bindFramebuffer();

        // save image before commit
        glm::vec2 box_sz = zw(bounds) - xy(bounds);
        bool has_data = box_sz.x > 0 && box_sz.y > 0;
        if (has_data)
        {
            action->m_image[i] = std::make_unique<uint8_t[]>(box_sz.x * box_sz.y * 4);
            glReadPixels(bounds.x, bounds.y, box_sz.x, box_sz.y, GL_RGBA, GL_UNSIGNED_BYTE, action->m_image[i].get());
            action->m_box[i] = bounds;
        }

        action->m_old_box[i] = layer.m_dirty_box[i];
        action->m_old_dirty[i] = layer.m_dirty_face[i];

        // draw the tmp layer into the actual layer
        if (has_data)
        {
            ShaderManager::use(kShader::Texture);
            ShaderManager::u_int(kShaderUniform::Tex, 0);
            ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-0.5f, 0.5f, -0.5f, 0.5f));
            glActiveTexture(GL_TEXTURE0);
            m_sampler_nearest.bind(0);
            rtt.bindTexture();
            m_plane.draw_fill();
            rtt.unbindTexture();
            
            layer.m_dirty_face[i] = true;
            layer.m_dirty_box[i] = { glm::min(xy(layer.m_dirty_box[i]), xy(bounds)), glm::max(zw(layer.m_dirty_box[i]), zw(bounds)) };
        }

        layer.m_rtt[i].unbindFramebuffer();
    }

    // save history
    action->m_layer_idx = m_current_layer_idx;
    action->m_canvas = this;
    //action->m_stroke = std::move(m_current_stroke);
    ActionManager::add(action);

    glDeleteRenderbuffers(1, &rboID);
    rtt.destroy();

    // restore viewport and clear color states
    blend ? glEnable(GL_BLEND) : glDisable(GL_BLEND);
    glViewport(vp[0], vp[1], vp[2], vp[3]);
    glClearColor(cc[0], cc[1], cc[2], cc[3]);
    glActiveTexture(GL_TEXTURE0);
}

void Canvas::draw_objects(std::function<void(const glm::mat4& camera, const glm::mat4& proj, int i)> observer)
{
    draw_objects(observer, *m_layers[m_current_layer_idx]);
}

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 Layer::destroy()
{
    for (int i = 0; i < 6; i++)
        m_rtt[i].destroy();
}

void Layer::optimize()
{
    int saved_bytes = 0;
    glBindTexture(GL_TEXTURE_2D, 0);
    for (int i = 0; i < 6; i++)
    {
        if (!m_dirty_face[i])
            continue;

        auto data = std::unique_ptr<glm::u8vec4[]>(reinterpret_cast<glm::u8vec4*>(m_rtt[i].readTextureData()));
        glm::ivec2 bbmin(w,h);
        glm::ivec2 bbmax(0);
        for (int y = 0; y < h; y++)
        {
            for (int x = 0; x < w; x++)
            {
                if (data[x + y * w].a > 0)
                {
                    bbmin = glm::min(bbmin, { x, y });
                    bbmax = glm::max(bbmax, { x + 1, y + 1 });
                }
            }
        }
        glm::vec2 bbsz = bbmax - bbmin;
        glm::vec2 old_size = zw(m_dirty_box[i]) - xy(m_dirty_box[i]);
        glm::vec2 diff;
        if (bbsz.x <= 0 || bbmax.y <= 0)
        {
            m_dirty_face[i] = false;
            m_dirty_box[i] = glm::vec4(0);
            diff = old_size;
        }
        else
        {
            m_dirty_box[i] = { bbmin, bbmax };

            diff = old_size - bbsz;
        }
        saved_bytes += (int)(diff.x * diff.y * 4);
    }
    LOG("optimized %d bytes", saved_bytes);
}

void Layer::restore(const Snapshot& snap)
{
    //clear({ 0, 0, 0, 0 });
    for (int i = 0; i < 6; i++)
    {
        if (snap.image[i] == nullptr || snap.m_dirty_face[i] == false)
            continue;
        
        m_dirty_box[i] = snap.m_dirty_box[i];
        m_dirty_face[i] = snap.m_dirty_face[i];

        // TODO: this should not be recreated here!
        // Sorry I messed up with this,
        // it's just a quick fix DON'T SHIP!!
        //m_rtt[i].recreate();
        
        m_rtt[i].bindTexture();
        glm::vec2 box_sz = zw(m_dirty_box[i]) - xy(m_dirty_box[i]);
        glTexSubImage2D(GL_TEXTURE_2D, 0,
            m_dirty_box[i].x, m_dirty_box[i].y,
            box_sz.x, box_sz.y, GL_RGBA, GL_UNSIGNED_BYTE,
            snap.image[i].get());
        m_rtt[i].unbindTexture();
        LOG("restore face %d - %d bytes (%dx%d)", i,
        (int)box_sz.x * (int)box_sz.y * 4, (int)box_sz.x, (int)box_sz.y);
    }
}

Layer::Snapshot Layer::snapshot()
{
    Snapshot snap;
    static int counter = 0;
    glBindTexture(GL_TEXTURE_2D, 0);
    //glBindFramebuffer(GL_FRAMEBUFFER, 0);
    for (int i = 0; i < 6; i++)
    {
        snap.m_dirty_box[i] = m_dirty_box[i];
        snap.m_dirty_face[i] = m_dirty_face[i];

        if (!m_dirty_face[i])
            continue;

        snap.image[i] = std::make_unique<uint8_t[]>(m_rtt[i].bytes());

        //glReadBuffer(GL_BACK);
        m_rtt[i].bindFramebuffer();
        glm::vec2 box_sz = zw(m_dirty_box[i]) - xy(m_dirty_box[i]);
        glReadPixels(m_dirty_box[i].x, m_dirty_box[i].y, box_sz.x, box_sz.y, GL_RGBA, GL_UNSIGNED_BYTE, snap.image[i].get());
        m_rtt[i].unbindFramebuffer();
        //glReadBuffer(GL_NONE);
    }
    counter++;
    return snap;
}

void Layer::clear(const glm::vec4& c)
{
    // push clear color state
    GLfloat cc[4];
    glGetFloatv(GL_COLOR_CLEAR_VALUE, cc);
    glClearColor(c.r, c.g, c.b, c.a);

    bool erase = (c.a == 0.f);

    for (int i = 0; i < 6; i++)
    {
        m_rtt[i].bindFramebuffer();
        glClear(GL_COLOR_BUFFER_BIT);
        m_rtt[i].unbindFramebuffer();

        if (erase)
        {
            m_dirty_box[i] = glm::vec4(w, h, 0, 0); // reset bounding box
            m_dirty_face[i] = false;
        }
        else
        {
            m_dirty_box[i] = glm::vec4(0, 0, w, h); // reset bounding box
            m_dirty_face[i] = true;
        }
    }

    // restore clear color state
    glClearColor(cc[0], cc[1], cc[2], cc[3]);
}

bool Layer::create(int width, int height, std::string name)
{
    m_name = name;
    w = width;
    h = height;
    for (int i = 0; i < 6; i++)
    {
        m_rtt[i].create(width, height);
        m_rtt[i].bindFramebuffer();
        m_rtt[i].clear();
        m_rtt[i].unbindFramebuffer();
        m_dirty_box[i] = glm::vec4(w, h, 0, 0); // reset bounding box
        m_dirty_face[i] = false;
    }
    return true;
}

void Layer::resize(int width, int height)
{
    glm::vec2 ratio = glm::vec2(width, height) / glm::vec2(w, h);
    w = width;
    h = height;
    for (int i = 0; i < 6; i++)
    {
        m_rtt[i].resize(width, height);
        m_dirty_box[i] = m_dirty_box[i] * glm::vec4(ratio, ratio);
        //m_dirty_face[i] = true;
    }
}

///////////////////////////////////////////////////////////////////////////////

void ActionStroke::undo()
{
    if (clear_layer)
        m_canvas->m_layers[m_layer_idx]->clear({ 0, 0, 0, 0 });
    for (int i = 0; i < 6; i++)
    {
        // empty data
        if (!m_image[i])
            continue;

        LOG("undo box %d dirty=%s [%d,%d,%d,%d] to dirty=%s [%d,%d,%d,%d]",
            i,
            m_canvas->m_layers[m_layer_idx]->m_dirty_face[i] ? "true" : "false",
            (int)m_canvas->m_layers[m_layer_idx]->m_dirty_box[i].x,
            (int)m_canvas->m_layers[m_layer_idx]->m_dirty_box[i].y,
            (int)m_canvas->m_layers[m_layer_idx]->m_dirty_box[i].z,
            (int)m_canvas->m_layers[m_layer_idx]->m_dirty_box[i].w,
            m_old_dirty[i] ? "true" : "false",
            (int)m_old_box[i].x,
            (int)m_old_box[i].y,
            (int)m_old_box[i].z,
            (int)m_old_box[i].w);
        m_canvas->m_layers[m_layer_idx]->m_dirty_box[i] = m_old_box[i];
        m_canvas->m_layers[m_layer_idx]->m_dirty_face[i] = m_old_dirty[i];


        glm::vec2 box_sz = zw(m_box[i]) - xy(m_box[i]);
        if (box_sz.x > 0 && box_sz.y > 0 && box_sz.x <= m_canvas->m_layers[m_layer_idx]->w && box_sz.y <= m_canvas->m_layers[m_layer_idx]->h)
        {
            m_canvas->m_layers[m_layer_idx]->m_rtt[i].bindTexture();
            glTexSubImage2D(GL_TEXTURE_2D, 0, (int)m_box[i].x, (int)m_box[i].y, (int)box_sz.x, (int)box_sz.y, GL_RGBA, GL_UNSIGNED_BYTE, m_image[i].get());
            m_canvas->m_layers[m_layer_idx]->m_rtt[i].unbindTexture();
        }
        else
        {
            LOG("undo invalid box size (%d, %d)", (int)box_sz.x, (int)box_sz.y);
        }
    }
}

size_t ActionStroke::memory()
{
    size_t mem = 0;
    for (int i = 0; i < 6; i++)
    {
        glm::ivec2 sz = zw(m_box[i]) - xy(m_box[i]);
        mem += sz.x * sz.y * 4 + sizeof(*this);
    }
    return mem;
}

Action* ActionStroke::get_redo()
{
    auto action = new ActionStroke;
    auto& layer = m_canvas->m_layers[m_layer_idx];
    for (int i = 0; i < 6; i++)
    {
        if (!layer->m_dirty_face[i] && !m_image[i])
            continue; // no stroke on this face, skip it

        layer->m_rtt[i].bindFramebuffer();

        // save image before commit
        glm::vec2 box_or = xy(m_box[i]);
        glm::vec2 box_sz = zw(m_box[i]) - xy(m_box[i]);
        if (box_sz.x > 0 && box_sz.y > 0 && box_sz.x <= layer->w && box_sz.y <= layer->h)
        {
            action->m_image[i] = std::make_unique<uint8_t[]>(box_sz.x * box_sz.y * 4);
            glReadPixels(box_or.x, box_or.y, box_sz.x, box_sz.y, GL_RGBA, GL_UNSIGNED_BYTE, action->m_image[i].get());
        }
        else
        {
            LOG("create_action invalid box size (%d, %d)", (int)box_sz.x, (int)box_sz.y);
        }

        action->m_box[i] = m_box[i];
        action->m_old_box[i] = layer->m_dirty_box[i];
        action->m_old_dirty[i] = layer->m_dirty_face[i];

        layer->m_rtt[i].unbindFramebuffer();
    }
    // save history
    action->m_layer_idx = m_layer_idx;
    action->m_canvas = m_canvas;
    //action->m_stroke = std::move(m_stroke);
    action->clear_layer = false;
    return action;
}
