#include "pch.h"
#include "log.h"
#include "app.h"
#include "node_icon.h"
#include "node_dialog_open.h"
#include "node_progress_bar.h"
#include "mp4enc.h"
#include "app_core/app_frame.h"
#include "app_core/app_shutdown.h"
#include "app_core/app_status.h"
#include "app_core/app_startup.h"
#include "app_core/app_thread.h"
#include "app_core/canvas_tool_ui.h"
#include "app_core/document_cloud.h"
#include "app_core/document_recording.h"
#include "app_core/document_route.h"
#include "app_core/document_session.h"
#include "legacy_app_startup_services.h"
#include "legacy_document_open_services.h"
#include "legacy_document_session_services.h"
#include "legacy_gl_runtime_dispatch.h"
#include "legacy_preference_storage.h"
#include "legacy_recording_services.h"
#include "legacy_ui_gl_dispatch.h"
#include "platform_api/platform_services.h"
#include "renderer_gl/opengl_capabilities.h"

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

App* App::I = nullptr; // singleton

std::deque<AppTask> App::render_tasklist;
std::mutex App::render_task_mutex;
std::condition_variable App::render_cv;

namespace {

pp::app::CanvasToolMode canvas_tool_mode_from_canvas_mode(kCanvasMode mode) noexcept
{
    switch (mode) {
    case kCanvasMode::Draw:
        return pp::app::CanvasToolMode::draw;
    case kCanvasMode::Erase:
        return pp::app::CanvasToolMode::erase;
    case kCanvasMode::Line:
        return pp::app::CanvasToolMode::line;
    case kCanvasMode::Camera:
        return pp::app::CanvasToolMode::camera;
    case kCanvasMode::Grid:
        return pp::app::CanvasToolMode::grid;
    case kCanvasMode::Copy:
        return pp::app::CanvasToolMode::copy;
    case kCanvasMode::Cut:
        return pp::app::CanvasToolMode::cut;
    case kCanvasMode::Fill:
        return pp::app::CanvasToolMode::fill;
    case kCanvasMode::MaskFree:
        return pp::app::CanvasToolMode::mask_free;
    case kCanvasMode::MaskLine:
        return pp::app::CanvasToolMode::mask_line;
    case kCanvasMode::FloodFill:
        return pp::app::CanvasToolMode::flood_fill;
    default:
        return pp::app::CanvasToolMode::draw;
    }
}

void apply_app_viewport(pp::renderer::gl::OpenGlViewportRect viewport)
{
    const auto status = pp::renderer::gl::apply_opengl_viewport(
        viewport,
        pp::renderer::gl::OpenGlViewportDispatch {
            .viewport = pp::legacy::ui_gl::set_opengl_viewport,
        });
    if (!status.ok())
        LOG("OpenGL viewport failed: %s", status.message);
}

void apply_app_scissor(pp::renderer::gl::OpenGlScissorRect scissor)
{
    const auto status = pp::renderer::gl::apply_opengl_scissor_rect(
        scissor,
        pp::renderer::gl::OpenGlScissorDispatch {
            .enable = pp::legacy::ui_gl::enable_opengl_state,
            .disable = pp::legacy::ui_gl::disable_opengl_state,
            .scissor = pp::legacy::ui_gl::set_opengl_scissor,
        });
    if (!status.ok())
        LOG("OpenGL scissor failed: %s", status.message);
}

void apply_app_scissor_test(bool enabled)
{
    const auto status = pp::renderer::gl::apply_opengl_scissor_test(
        enabled,
        pp::renderer::gl::OpenGlScissorTestDispatch {
            .enable = pp::legacy::ui_gl::enable_opengl_state,
            .disable = pp::legacy::ui_gl::disable_opengl_state,
        });
    if (!status.ok())
        LOG("OpenGL scissor test failed: %s", status.message);
}

[[nodiscard]] GLenum linear_texture_filter() noexcept
{
    return static_cast<GLenum>(pp::renderer::gl::linear_texture_filter());
}

[[nodiscard]] GLenum nearest_texture_filter() noexcept
{
    return static_cast<GLenum>(pp::renderer::gl::nearest_texture_filter());
}

[[nodiscard]] GLenum repeat_texture_wrap() noexcept
{
    return static_cast<GLenum>(pp::renderer::gl::repeat_texture_wrap());
}

[[nodiscard]] GLenum framebuffer_target() noexcept
{
    return static_cast<GLenum>(pp::renderer::gl::framebuffer_target());
}

[[nodiscard]] GLuint default_framebuffer_id() noexcept
{
    return static_cast<GLuint>(pp::renderer::gl::default_framebuffer_id());
}

}
std::thread App::render_thread;
std::thread::id App::render_thread_id;
bool App::render_running = false;

std::deque<AppTask> App::ui_tasklist;
std::mutex App::ui_task_mutex;
std::condition_variable App::ui_cv;
std::thread App::ui_thread;
std::thread::id App::ui_thread_id;
bool App::ui_running = false;

void App::create()
{
    const auto initial_surface = pp::app::plan_app_initial_surface();
    width = initial_surface.width;
    height = initial_surface.height;
}

void App::open_document(std::string path)
{
    const auto route = pp::app::classify_document_open_path(path);
    if (!route)
        return;

    const bool has_unsaved_project =
        route.value().kind == pp::app::DocumentOpenKind::open_project && Canvas::I->m_unsaved;
    const auto open_plan = pp::app::plan_document_open(route.value().kind, has_unsaved_project);
    const auto status = pp::panopainter::execute_legacy_document_open_plan(*this, open_plan, route.value());
    if (!status.ok())
        LOG("Document open action failed: %s", status.message);
}

bool App::request_close()
{
    static bool dialog_already_opened = false;
    const auto close_decision = pp::app::plan_close_request(
        Canvas::I->m_unsaved,
        dialog_already_opened);
    const auto status = pp::panopainter::execute_legacy_close_request_decision(
        *this,
        close_decision,
        dialog_already_opened);
    if (!status.ok())
        LOG("Close request action failed: %s", status.message);

    return close_decision == pp::app::CloseRequestDecision::close_now;
}

void App::clear()
{
    const auto status = pp::renderer::gl::clear_panopainter_default_target(
        pp::renderer::gl::OpenGlClearDispatch {
            .clear_color = pp::legacy::ui_gl::set_opengl_clear_color,
            .clear = pp::legacy::ui_gl::clear_opengl_buffer,
        });
    if (!status.ok())
        LOG("OpenGL clear failed: %s", status.message);
}

void App::initAssets()
{
    LOG("initializing assets");
    FontManager::init();

    LOG("initializing assets create sampler");
    sampler.create(nearest_texture_filter());
    sampler_stencil.create(linear_texture_filter(), repeat_texture_wrap());
    sampler_linear.create(linear_texture_filter());
    m_face_plane.create<1>(2, 2);
    sphere.create<8, 8>(1);
    LOG("initializing assets load uvs texture");
    LOG("initializing assets completed");
}

void App::initLog()
{
    const auto paths = prepare_storage_paths();
    if (!paths.data_path.empty())
        data_path = paths.data_path;
    if (!paths.recording_path.empty())
        rec_path = paths.recording_path;
    if (!paths.temporary_path.empty())
        tmp_path = paths.temporary_path;

    // TODO: save this path somewhere in the settings, don't overwrite every start
    work_path = paths.work_path.empty() ? data_path : paths.work_path;

    //LogRemote::I.start();
    LogRemote::I.file_init();
    LOG("%s", g_version);

    LOG("load preferences");
    if (!pp::panopainter::load_legacy_preferences())
        LOG("load preferences failed");
}

#if WITH_CURL
int progress_callback_download(void *clientp, curl_off_t dltotal,
    curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow)
{
    (void)ultotal;
    (void)ulnow;
    auto* progress = static_cast<std::function<void(float)>*>(clientp);
    const auto plan = pp::app::plan_cloud_transfer_progress(
        static_cast<std::int64_t>(dltotal),
        static_cast<std::int64_t>(dlnow));
    if (progress != nullptr && *progress && plan.notify)
        (*progress)(plan.fraction);
    return 0;
}

int progress_callback_upload(void *clientp, curl_off_t dltotal,
    curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow)
{
    (void)dltotal;
    (void)dlnow;
    auto* progress = static_cast<std::function<void(float)>*>(clientp);
    const auto plan = pp::app::plan_cloud_transfer_progress(
        static_cast<std::int64_t>(ultotal),
        static_cast<std::int64_t>(ulnow));
    if (progress != nullptr && *progress && plan.notify)
        (*progress)(plan.fraction);
    return 0;
}
#endif //CURL

void App::download(std::string url, std::string dest_filepath, std::function<void(float)> progress)
{
#if WITH_CURL
    const auto plan = pp::app::plan_cloud_download_transfer(
        url,
        dest_filepath,
        progress != nullptr,
        disables_network_tls_verification());
    if (plan.action != pp::app::CloudTransferAction::start_transfer) {
        LOG("download skipped: invalid transfer request");
        return;
    }

    CURL *curl = curl_easy_init();
    if (curl)
    {
        FILE* fp = fopen(dest_filepath.c_str(), "wb");
        if (fp == nullptr) {
            LOG("download failed to open destination %s", dest_filepath.c_str());
            curl_easy_cleanup(curl);
            return;
        }
        LOG("download %s to %s", url.c_str(), dest_filepath.c_str());
        curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_data_write);
        if (plan.disable_tls_verification)
            curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
        if (plan.enable_progress)
        {
            curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, progress_callback_download);
            curl_easy_setopt(curl, CURLOPT_XFERINFODATA, &progress);
            curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0);
        }
        auto err = curl_easy_perform(curl);
        curl_easy_cleanup(curl);
        fclose(fp);
    }
#endif //CURL
}

bool App::check_license()
{
    return true; // TODO: for distribuiton only

#if WITH_CURL
    CURL *curl = curl_easy_init();
    if (curl)
    {
        std::string url = "https://panopainter.com/license/7565D057-ACBE-4721-9A4E-F342D3DDB7D8.php";
        //std::string url = "https://panopainter.com/license/E8EDC2FE-E1BD-4AB1-91BD-FCCD926739BD.php"; // wacom
        //std::string url = "https://panopainter.com/license/A744FBA9-BC6C-43C8-BD24-0CCE24B3D985.php"; // others

        std::string ret;
        curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
        curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L);

        curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ret);
        //curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_data_handler);
        curl_easy_setopt(curl, CURLOPT_TIMEOUT, 2L);
        if (disables_network_tls_verification())
            curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);

        auto err = curl_easy_perform(curl);
        curl_easy_cleanup(curl);

        LOG("License check: %s", ret.c_str());
        if (err == CURLcode::CURLE_OK && ret == "success")
            return true;
    }
#endif //CURL
    return false;
}

void App::upload(std::string filename, std::string name, std::function<void(float)> progress)
{
#if WITH_CURL
    const auto plan = pp::app::plan_cloud_upload_transfer(
        filename,
        progress != nullptr,
        disables_network_tls_verification());
    if (plan.action != pp::app::CloudTransferAction::start_transfer) {
        LOG("upload skipped: invalid transfer request");
        return;
    }

    CURL *curl;

    struct curl_httppost *formpost = NULL;
    struct curl_httppost *lastptr = NULL;

    //curl_global_init(CURL_GLOBAL_ALL);

    curl_formadd(&formpost,
        &lastptr,
        CURLFORM_COPYNAME, "fileToUpload",
        CURLFORM_FILE, filename.c_str(),
        CURLFORM_END);

    curl = curl_easy_init();
    std::string res;

    if (curl) 
    {
        std::string url = "https://panopainter.com/cloud/cloud-upl.php?name=" + name;
        curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
        curl_easy_setopt(curl, CURLOPT_HTTPPOST, formpost);
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, &res);
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_data_handler);
        if (plan.disable_tls_verification)
            curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
        if (plan.enable_progress)
        {
            curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, progress_callback_upload);
            curl_easy_setopt(curl, CURLOPT_XFERINFODATA, &progress);
            curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0);
        }
        auto err = curl_easy_perform(curl);
        std::cout << "\n\nUPLOAD RESULT\n" << res << "\n\n\n";
        curl_easy_cleanup(curl);
    }
#endif //CURL
}

void App::init()
{
    LOG("Screen Resolution: %dx%d", (int)width, (int)height);

    render_task([]
    {
        App::I->install_render_debug_callback();
        const auto runtime_info_result = pp::renderer::gl::query_opengl_runtime_info(
            pp::legacy::gl_runtime::runtime_info_dispatch());
        if (runtime_info_result.ok())
        {
            const auto& runtime_info = runtime_info_result.value();
            LOG("GL version: %s", runtime_info.version);
            LOG("GL vendor: %s", runtime_info.vendor);
            LOG("GL renderer: %s", runtime_info.renderer);
            LOG("GLSL version: %s", runtime_info.shading_language_version);
        }
        else
        {
            LOG("OpenGL runtime info failed: %s", runtime_info_result.status().message);
        }

        //GLint n_exts;
        //glGetIntegerv(GL_NUM_EXTENSIONS, &n_exts);
        //for (int i = 0; i < n_exts; i++)
        //{
        //    std::string ext = (const char*)glGetStringi(GL_EXTENSIONS, i);
        //    //if (ext.find("debug") != std::string::npos)
        //    {
        //        LOG("%s", glGetStringi(GL_EXTENSIONS, i));
        //    }
        //}
    
        App::I->apply_render_platform_hints();
        const auto startup_state_status = pp::renderer::gl::apply_panopainter_initial_state(
            pp::renderer::gl::OpenGlStateDispatch {
                .enable = pp::legacy::ui_gl::enable_opengl_state,
                .disable = pp::legacy::ui_gl::disable_opengl_state,
                .blend_func = pp::legacy::ui_gl::set_opengl_blend_func,
                .blend_equation_separate = pp::legacy::ui_gl::set_opengl_blend_equation_separate,
            });
        if (!startup_state_status.ok())
            LOG("OpenGL startup state failed: %s", startup_state_status.message);
    });

    const auto startup_preferences =
        pp::panopainter::read_legacy_startup_preferences(vr_controllers_enabled);
    const auto startup_plan = pp::app::plan_app_startup(
        startup_preferences.run_counter,
        startup_preferences.auto_timelapse,
        startup_preferences.vr_controllers_enabled,
        check_license());
    if (!startup_plan) {
        LOG("App startup plan failed: %s", startup_plan.status().message);
    } else {
        const auto persistence_status = pp::panopainter::execute_legacy_app_startup_persistence_plan(
            *this,
            startup_plan.value());
        if (!persistence_status.ok())
            LOG("App startup persistence failed: %s", persistence_status.message);
    }

    const auto startup_resources = pp::app::plan_app_startup_resources(width, height);
    if (!startup_resources) {
        LOG("App startup resource plan failed: %s", startup_resources.status().message);
    } else {
        const auto resource_status = pp::panopainter::execute_legacy_app_startup_resources(
            *this,
            startup_resources.value());
        if (!resource_status.ok())
            LOG("App startup resources failed: %s", resource_status.message);
    }

    if (startup_plan) {
        const auto startup_status = pp::panopainter::execute_legacy_app_startup_runtime_plan(
            *this,
            startup_plan.value());
        if (!startup_status.ok())
            LOG("App startup runtime execution failed: %s", startup_status.message);
    }
}

void App::async_start()
{
    acquire_render_context();
}

void App::async_redraw()
{
    const auto plan = pp::app::plan_app_async_redraw();
    if (plan.set_redraw)
        redraw = true;
    if (plan.notify_ui)
        ui_cv.notify_all();
}

void App::async_end()
{
    release_render_context();
}

void App::async_swap()
{
    present_render_context();
}

bool App::update_ui_observer(Node *n)
{
    std::vector<pp::app::AppUiObserverParentClip> parent_clips;
    if (n) {
        for (Node* p = n->m_parent; p; p = p->m_parent) {
            parent_clips.push_back(pp::app::AppUiObserverParentClip {
                .clip = pp::app::AppUiObserverRect {
                    .x = p->m_clip_uncut.x,
                    .y = p->m_clip_uncut.y,
                    .width = p->m_clip_uncut.z,
                    .height = p->m_clip_uncut.w,
                },
                .padding_top = YGNodeLayoutGetPadding(p->y_node, YGEdgeTop),
                .padding_right = YGNodeLayoutGetPadding(p->y_node, YGEdgeRight),
                .padding_bottom = YGNodeLayoutGetPadding(p->y_node, YGEdgeBottom),
                .padding_left = YGNodeLayoutGetPadding(p->y_node, YGEdgeLeft),
            });
        }
    }

    const auto plan = pp::app::plan_app_ui_observer(
        n != nullptr,
        n && n->m_display,
        n && n->m_on_screen,
        n
            ? pp::app::AppUiObserverRect {
                .x = n->m_clip_uncut.x,
                .y = n->m_clip_uncut.y,
                .width = n->m_clip_uncut.z,
                .height = n->m_clip_uncut.w,
            }
            : pp::app::AppUiObserverRect {},
        parent_clips,
        height,
        zoom,
        off_x,
        off_y);
    if (!plan) {
        LOG("UI observer plan failed: %s", plan.status().message);
        return false;
    }

    if (!n)
        return false;

    if (plan.value().notify_leave_screen)
        n->handle_on_screen(true, false);
    if (plan.value().notify_enter_screen)
        n->handle_on_screen(false, true);
    n->m_on_screen = plan.value().next_on_screen;

    if (!plan.value().draw_node)
        return false;

    apply_app_scissor(pp::renderer::gl::OpenGlScissorRect {
        .enabled = 1U,
        .x = plan.value().scissor_x,
        .y = plan.value().scissor_y,
        .width = plan.value().scissor_width,
        .height = plan.value().scissor_height,
    });
    n->draw();
    return true;
}

void App::draw(float dt)
{
    const auto draw_plan = pp::app::plan_app_frame_draw(
        canvas != nullptr,
        canvas && canvas->m_canvas,
        vr_active,
        ui_visible,
        vr_only);

    // update offscreen stuff
    if (draw_plan.draw_canvas_stroke)
        canvas->m_canvas->stroke_draw();
    
    auto observer = std::bind(&App::update_ui_observer, this, std::placeholders::_1);

    if (draw_plan.draw_vr_ui)
    {
        uirtt.bindFramebuffer();
        uirtt.clear();
        apply_app_viewport(pp::renderer::gl::OpenGlViewportRect {
            .width = static_cast<std::int32_t>(uirtt.getWidth()),
            .height = static_cast<std::int32_t>(uirtt.getHeight()),
        });
        apply_app_scissor_test(true);
        for (int i = 1; i < layout[main_id]->m_children.size(); i++)
            layout[main_id]->m_children[i]->watch(observer);
        for (int i = 0; layout_designer.get(main_id) && i < layout_designer[main_id]->m_children.size(); i++)
            layout_designer[main_id]->m_children[i]->watch(observer);
        //msgbox->watch(observer);
        apply_app_scissor_test(false);
        uirtt.unbindFramebuffer();
    }

    if (draw_plan.draw_main_ui)
    {
        bind_main_render_target();
        apply_app_viewport(pp::renderer::gl::OpenGlViewportRect {
            .x = static_cast<std::int32_t>(off_x),
            .y = static_cast<std::int32_t>(off_y),
            .width = static_cast<std::int32_t>(width),
            .height = static_cast<std::int32_t>(height),
        });
        apply_app_scissor_test(true);
        for (int i = 0; i < layout[main_id]->m_children.size(); i++)
            layout[main_id]->m_children[i]->watch(observer);
        for (int i = 0; layout_designer.get(main_id) && i < layout_designer[main_id]->m_children.size(); i++)
            layout_designer[main_id]->m_children[i]->watch(observer);
        //msgbox->watch(observer);
        apply_app_scissor_test(false);
    }

    if (draw_plan.reset_redraw)
        redraw = false;
}

void App::update(float dt)
{
    static std::mutex mutex;

    // avoid multiple threads to update the scene
    //std::lock_guard<std::mutex> lock(mutex);
    
    const auto update_plan = pp::app::plan_app_frame_update(redraw, animate);
    if (!update_plan.update_frame)
        return;

    if (auto* main = layout[main_id]; update_plan.update_layouts && main)
        main->update(width, height, zoom);

    if (auto* main = layout_designer[main_id]; update_plan.update_layouts && main)
        main->update(width, height, zoom);

    if (!update_plan.refresh_canvas_toolbar)
        return;

    {
        auto mode = Canvas::I->m_current_mode;

        CanvasModePen* pm = (CanvasModePen*)canvas->m_canvas->modes[(int)kCanvasMode::Draw][0];
        const auto toolbar = pp::app::plan_canvas_tool_button_state(
            canvas_tool_mode_from_canvas_mode(mode),
            pm && pm->m_picking,
            canvas->m_canvas->m_touch_lock);
        layout[main_id]->find<NodeButtonCustom>("btn-pick")->set_active(toolbar.pick_active);
        layout[main_id]->find<NodeButtonCustom>("btn-touchlock")->set_active(toolbar.touch_lock_active);

        layout[main_id]->find<NodeButtonCustom>("btn-pen")->set_active(toolbar.pen_active);
        layout[main_id]->find<NodeButtonCustom>("btn-erase")->set_active(toolbar.erase_active);
        layout[main_id]->find<NodeButton>("btn-cam")->set_active(toolbar.camera_active);
        layout[main_id]->find<NodeButtonCustom>("btn-line")->set_active(toolbar.line_active);
        layout[main_id]->find<NodeButton>("btn-grid")->set_active(toolbar.grid_active);
        layout[main_id]->find<NodeButton>("btn-copy")->set_active(toolbar.copy_active);
        layout[main_id]->find<NodeButton>("btn-cut")->set_active(toolbar.cut_active);
        layout[main_id]->find<NodeButtonCustom>("btn-mask-free")->set_active(toolbar.mask_free_active);
        layout[main_id]->find<NodeButtonCustom>("btn-mask-line")->set_active(toolbar.mask_line_active);
        layout[main_id]->find<NodeButtonCustom>("btn-bucket")->set_active(toolbar.flood_fill_active);
    }
}

void App::terminate()
{
    LOG("App::terminate");
    const auto shutdown_plan = pp::app::plan_app_shutdown();

    if (shutdown_plan.save_ui_state)
        ui_save();

    if (shutdown_plan.terminate_stroke_preview_renderer)
        NodeStrokePreview::terminate_renderer();
    if (shutdown_plan.stop_recording)
        rec_stop();
    
    if (shutdown_plan.invalidate_textures)
        TextureManager::invalidate();
    if (shutdown_plan.invalidate_shaders)
        ShaderManager::invalidate();
    if (shutdown_plan.unload_layouts) {
        layout.unload();
        layout_designer.unload();
    }
    if (shutdown_plan.destroy_ui_render_target)
        uirtt.destroy();
    if (shutdown_plan.destroy_face_plane)
        m_face_plane.destroy();
    if (shutdown_plan.release_panel_nodes) {
        layers.reset();
        color.reset();
        stroke.reset();
        grid.reset();
        presets.reset();
        floating_presets.reset();
        floating_color.reset();
        floating_layers.reset();
        floating_picker.reset();
    }
    if (shutdown_plan.clear_quick_mode_state)
        quick_mode_state.clear();
}

void App::update_memory_usage(size_t bytes)
{
    if (auto txt = layout[main_id]->find<NodeText>("txt-memory"))
    {
        const auto label = pp::app::make_history_memory_label(bytes);
        txt->set_text(label.c_str());
    }
}

void App::update_rec_frames()
{
    if (auto txt = layout[main_id]->find<NodeText>("txt-rec"))
    {
        const auto label = pp::app::make_recording_frame_label(
            rec_running,
            Canvas::I->m_encoder != nullptr,
            Canvas::I->m_encoder ? Canvas::I->m_encoder->frames_count() : 0);
        txt->set_text(label.text.c_str());
    }
}

int App::res_from_index(int i)
{
    const auto resolution = pp::app::display_resolution_from_index(i);
    return resolution ? resolution.value() : pp::app::document_resolution_values.front();
}

int App::res_to_index(int res)
{
    const auto index = pp::app::document_resolution_to_index(res);
    return index ? static_cast<int>(index.value()) : static_cast<int>(pp::app::document_resolution_values.size());
}

std::string App::res_to_string(int res)
{
    const auto label = pp::app::document_resolution_label(res);
    return label ? std::string(label.value()) : std::string("unknown");
}

void App::renderdoc_frame_start()
{
    begin_render_capture_frame();
}

void App::renderdoc_frame_end()
{
    end_render_capture_frame();
}

void App::rec_clear()
{
    const auto plan = pp::app::plan_recording_clear(
        rec_running,
        platform_deletes_recorded_files_on_clear()
    );
    const auto status = pp::panopainter::execute_legacy_recording_clear_plan(*this, plan);
    if (!status.ok())
        LOG("Recording clear action failed: %s", status.message);
}

void App::rec_start()
{
    const auto plan = pp::app::plan_recording_start(rec_running);
    const auto status = pp::panopainter::execute_legacy_recording_start_action(*this, plan);
    if (!status.ok())
        LOG("Recording start action failed: %s", status.message);
}

void App::rec_stop()
{
    const auto plan = pp::app::plan_recording_stop(rec_running);
    const auto status = pp::panopainter::execute_legacy_recording_stop_action(*this, plan);
    if (!status.ok())
        LOG("Recording stop action failed: %s", status.message);
}

void App::rec_export(std::string path)
{
    const auto plan = pp::app::plan_recording_export(static_cast<std::size_t>(rec_count));
/*
#if defined(__IOS__) || defined(__OSX__)
    export_mp4(rec_path, width, height, rec_count, ^(float) {
        pb->increment();
    });
#endif
*/
    const auto status = pp::panopainter::execute_legacy_recording_export_plan(*this, plan, path);
    if (!status.ok())
        LOG("Recording export action failed: %s", status.message);
}

void App::rec_loop()
{
    BT_SetTerminate();
    rec_running = true;
    while(rec_running)
    {
        std::unique_lock<std::mutex> lock(rec_mutex);
        rec_cv.wait(lock/*, [this] { return !(rec_frames.empty() && rec_running); }*/);
        auto* legacy_canvas = Canvas::I;
        auto* canvas_document = canvas ? canvas->m_canvas.get() : nullptr;
        auto* encoder = legacy_canvas ? legacy_canvas->m_encoder.get() : nullptr;
        const auto plan = pp::app::plan_recording_worker_iteration(
            rec_running,
            encoder != nullptr,
            legacy_canvas != nullptr && canvas_document != nullptr);
        if (!plan.continue_running)
            break;

        if (plan.encode_frame && legacy_canvas && canvas_document && encoder)
        {
            if (plan.clear_dirty_stroke)
                canvas_document->m_dirty_stroke = false;
            PBO equirect = legacy_canvas->m_layers_merge.gen_equirect_pbo(
                encoder->frame_size());
            std::this_thread::yield();
            ImageRef img;
            img.create(equirect.width, equirect.height, equirect.map());
            encoder->encode(img);
            equirect.unmap();
            LOG("rec frame encoded");
            if (plan.update_frame_label)
                update_rec_frames();
        }
    }
}

void App::render_thread_tick()
{
    static uint32_t count = 0;
    render_thread_id = std::this_thread::get_id();
    std::deque<AppTask> working_list;
    pp::app::AppQueueDrainPlan drain_plan;

    // move the task list locally to free the queue for other threads
    {
        std::unique_lock<std::mutex> lock(render_task_mutex);
        drain_plan = pp::app::plan_app_render_queue_drain(render_tasklist.size());
        render_running = drain_plan.mark_running;
        if (!drain_plan.drain_tasks)
            return;
        working_list = std::move(render_tasklist);
    }

    // execute the tasks
    if (drain_plan.wrap_in_render_context)
    {
        async_start();
        while (!working_list.empty())
        {
            //LOG("render task %d", count);
            //LOG("task %s", working_list.front().name.c_str());
            count++;
            working_list.front()();
            working_list.pop_front();
        }
        async_end();
    }
}

void App::render_thread_main()
{
    BT_SetTerminate();

    uint32_t count = 0;
    render_thread_id = std::this_thread::get_id();
    render_running = pp::app::plan_app_thread_start().mark_running;
    while (render_running)
    {
        std::deque<AppTask> working_list;
        pp::app::AppQueueDrainPlan drain_plan;

        // move the task list locally to free the queue for other threads
        {
            std::unique_lock<std::mutex> lock(render_task_mutex);
            render_cv.wait(lock, [this] { return render_tasklist.empty() && render_running ? false : true; });
            drain_plan = pp::app::plan_app_render_queue_drain(render_tasklist.size());
            working_list = std::move(render_tasklist);
        }

        // execute the tasks
        if (drain_plan.wrap_in_render_context)
        {
            async_start();
            while (!working_list.empty())
            {
                //LOG("render task %d", count);
                //LOG("task %s", working_list.front().name.c_str());
                count++;
                working_list.front()();
                working_list.pop_front();
            }
            async_end();
        }
    }
}

void App::ui_thread_tick()
{
    ui_thread_id = std::this_thread::get_id();

    std::deque<AppTask> working_list;
    pp::app::AppUiTickPlan tick_plan;

    // move the task list locally to free the queue for other threads
    {
        std::unique_lock<std::mutex> lock(ui_task_mutex);
        tick_plan = pp::app::plan_app_ui_thread_tick(ui_tasklist.size(), redraw);
        ui_running = tick_plan.mark_running;
        working_list = std::move(ui_tasklist);
    }

    // execute the tasks
    if (tick_plan.execute_tasks)
    {
        while (!working_list.empty())
        {
            //LOG("ui task %d", count);
            working_list.front()();
            working_list.pop_front();
        }
    }

    if (tick_plan.tick_app)
        tick(0);

    const auto redraw_plan = pp::app::plan_app_ui_loop_redraw(redraw, 0);
    if (redraw_plan.enqueue_render_frame)
    {
        if (redraw_plan.update_before_render)
            update(0);
        render_task([this]
        {
            bind_default_render_target();
            clear();
            draw(0);
            async_swap();
        });
    }
}

void App::ui_thread_main()
{
    BT_SetTerminate();

    uint32_t count = 0;
    ui_thread_id = std::this_thread::get_id();
    ui_running = pp::app::plan_app_thread_start().mark_running;

    attach_ui_thread();

    LOG("ui thread init()");
    init();

    auto t_start = std::chrono::high_resolution_clock::now();
    float t_frame = 0;
    float t_fps_counter = 0;
    float t_reloader = 0;
    int rendered_frames = 0;
    while (ui_running)
    {
        std::deque<AppTask> working_list;

        // move the task list locally to free the queue for other threads
        {
            std::unique_lock<std::mutex> lock(ui_task_mutex);
            ui_cv.wait_for(lock, std::chrono::milliseconds(idle_ms), 
                [this] { return ui_tasklist.empty() && ui_running ? false : true; });
            working_list = std::move(ui_tasklist);
        }

        // execute the tasks
        if (!working_list.empty())
        {
            while (!working_list.empty())
            {
                //LOG("ui task %d", count);
                count++;
                working_list.front()();
                working_list.pop_front();
            }
        }

        auto t_now = std::chrono::high_resolution_clock::now();
        float dt = std::chrono::duration<float>(t_now - t_start).count();
        t_start = t_now;

        const auto timer_plan = pp::app::plan_app_ui_loop_timers(
            dt,
            t_frame,
            t_fps_counter,
            t_reloader,
            rendered_frames,
            platform_enables_live_asset_reloading());
        if (timer_plan) {
            if (timer_plan.value().update_platform_frame)
                update_platform_frame(dt);
            t_frame = timer_plan.value().frame_accumulator;
            t_fps_counter = timer_plan.value().fps_accumulator;
            t_reloader = timer_plan.value().reload_accumulator;
            rendered_frames = timer_plan.value().rendered_frames_after_report;

            if (timer_plan.value().report_rendered_frames)
                report_rendered_frames(timer_plan.value().reported_frame_count);

            if (timer_plan.value().check_live_asset_reload) {
                if (ShaderManager::reload())
                {
                    stroke->update_controls();
                    redraw = true;
                }
                if (layout.reload())
                    redraw = true;
                if (layout_designer.reload())
                    redraw = true;
            }
        }

        const auto redraw_plan = pp::app::plan_app_ui_loop_redraw(redraw, rendered_frames);
        if (redraw_plan.tick_app)
            tick(dt);

        if (redraw_plan.enqueue_render_frame)
        {
            if (redraw_plan.update_before_render)
                update(t_frame);
            render_task([this, t_frame]
            {
                bind_default_render_target();
                clear();
                draw(t_frame);
                async_swap();
            });
            if (redraw_plan.reset_frame_accumulator)
                t_frame = 0;
            rendered_frames = redraw_plan.rendered_frames;
        }
    }
    detach_ui_thread();
}

void App::render_thread_start()
{
    const auto plan = pp::app::plan_app_thread_start();
    if (plan.start_thread)
        render_thread = std::thread(&App::render_thread_main, this);
    render_running = plan.mark_running;
}

void App::render_thread_stop()
{
    const auto plan = pp::app::plan_app_thread_stop(render_thread.joinable());
    if (plan.mark_not_running)
        render_running = false;
    if (plan.notify_worker)
        render_cv.notify_all();
    if (plan.join_thread)
        render_thread.join();
}

void App::ui_thread_start()
{
    const auto plan = pp::app::plan_app_thread_start();
    if (plan.start_thread)
        ui_thread = std::thread(&App::ui_thread_main, this);
    ui_running = plan.mark_running;
}

void App::ui_thread_stop()
{
    const auto plan = pp::app::plan_app_thread_stop(ui_thread.joinable());
    if (plan.mark_not_running)
        ui_running = false;
    if (plan.notify_worker)
        ui_cv.notify_all();
    if (plan.join_thread)
        ui_thread.join();
}
