#include "pch.h"

#include "log.h"
#include "canvas.h"
#include "app.h"
#include "legacy_canvas_draw_merge_services.h"
#include "legacy_ui_gl_dispatch.h"
#include "legacy_ui_overlay_services.h"
#include "app_core/document_canvas.h"
#include "texture.h"
#include "node_progress_bar.h"
#include "paint_renderer/compositor.h"
#include "renderer_gl/opengl_capabilities.h"
#include "util.h"
#include <array>
#include <cstdint>
#include <numeric>

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

namespace {

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

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

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

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

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

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

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

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

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

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

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

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

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

} // namespace

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

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

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

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

    m_unsaved = true;

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

void Canvas::export_equirectangular(std::string file_path, std::function<void()> on_complete)
{
    if (App::I->check_license())
    {
        App::I->runtime().canvas_async_task([this, file_path = std::move(file_path), on_complete = std::move(on_complete)]() mutable {
            BT_SetTerminate();
            export_equirectangular_thread(file_path);
            if (on_complete)
                App::I->ui_task([on_complete = std::move(on_complete)]() mutable { on_complete(); });
        });
    }
}

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

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

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

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

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

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

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

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

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

}

void Canvas::export_depth(std::string file_name, std::function<void()> on_complete)
{
    if (App::I->check_license())
    {
        App::I->runtime().canvas_async_task([this, file_name = std::move(file_name), on_complete = std::move(on_complete)]() mutable {
            BT_SetTerminate();
            export_depth_thread(file_name);
            if (on_complete)
                App::I->ui_task([on_complete = std::move(on_complete)]() mutable { on_complete(); });
        });
    }
}

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

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

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

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

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

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

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

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

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

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

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

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

    rtt.destroy();
}

void Canvas::export_layers(std::string path, std::function<void()> on_complete)
{
    if (App::I->check_license())
    {
        App::I->runtime().canvas_async_task([this, path = std::move(path), on_complete = std::move(on_complete)]() mutable {
            BT_SetTerminate();
            export_layers_thread(path);
            if (on_complete)
                App::I->ui_task([on_complete = std::move(on_complete)]() mutable { on_complete(); });
        });
    }
}

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

void Canvas::export_anim_frames(std::string path, std::function<void()> on_complete)
{
    if (App::I->check_license())
    {
        App::I->runtime().canvas_async_task([this, path = std::move(path), on_complete = std::move(on_complete)]() mutable {
            BT_SetTerminate();
            export_anim_frames_thread(path);
            if (on_complete)
                App::I->ui_task([on_complete = std::move(on_complete)]() mutable { on_complete(); });
        });
    }
}

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

void Canvas::export_anim_mp4(std::string path, std::function<void()> on_complete)
{
    if (App::I->check_license())
    {
        App::I->runtime().canvas_async_task([this, path = std::move(path), on_complete = std::move(on_complete)]() mutable {
            BT_SetTerminate();
            export_anim_mp4_thread(path);
            if (on_complete)
                App::I->ui_task([on_complete = std::move(on_complete)]() mutable { on_complete(); });
        });
    }
}

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

void Canvas::export_cube_faces(std::string file_name, std::function<void()> on_complete)
{
    if (App::I->check_license())
    {
        App::I->runtime().canvas_async_task([this, file_name = std::move(file_name), on_complete = std::move(on_complete)]() mutable {
            BT_SetTerminate();
            export_cube_faces_thread(file_name);
            if (on_complete)
                App::I->ui_task([on_complete = std::move(on_complete)]() mutable { on_complete(); });
        });
    }
}

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

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

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

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

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

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

void Canvas::project_save(std::function<void(bool)> on_complete)
{
    if (App::I->check_license())
    {
        const auto file_path = App::I->doc_path;
        App::I->runtime().canvas_async_task([this, file_path, on_complete = std::move(on_complete)]() mutable {
            BT_SetTerminate();
            bool ret = project_save_thread(file_path, true);
            if (on_complete)
                App::I->ui_task([on_complete = std::move(on_complete), ret]() mutable { on_complete(ret); });
        });
    }
}

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())
    {
        App::I->runtime().canvas_async_task([this, file_path = std::move(file_path), on_complete = std::move(on_complete)]() mutable {
            BT_SetTerminate();
            bool ret = project_save_thread(file_path, true);
            if (on_complete)
                App::I->ui_task([on_complete = std::move(on_complete), ret]() mutable { on_complete(ret); });
        });
    }
    else
    {
        LOG("no license, no save");
    }
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    fclose(fp);

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

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

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

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

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

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

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

void Canvas::project_open(std::string file_path, std::function<void(bool)> on_complete)
{
    App::I->runtime().canvas_async_task([this, file_path = std::move(file_path), on_complete = std::move(on_complete)]() mutable {
        BT_SetTerminate();
        bool result = project_open_thread(file_path);
        if (on_complete)
            App::I->ui_task([on_complete = std::move(on_complete), result]() mutable { on_complete(result); });
    });
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    std::swap(tmp_layers, m_layers);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            blendtex.unbind();
        }

        fb.unbindFramebuffer();

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

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

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

    return image;
}

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

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

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

