#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_status.h"
#include "app_core/app_startup.h"
#include "app_core/canvas_tool_ui.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_recording_services.h"
#include "platform_api/platform_services.h"
#include "renderer_gl/opengl_capabilities.h"

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

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

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

namespace {

[[nodiscard]] const char* query_opengl_string(std::uint32_t name) noexcept
{
    return reinterpret_cast<const char*>(glGetString(static_cast<GLenum>(name)));
}

void enable_opengl_state(std::uint32_t state) noexcept
{
    glEnable(static_cast<GLenum>(state));
}

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 disable_opengl_state(std::uint32_t state) noexcept
{
    glDisable(static_cast<GLenum>(state));
}

void set_opengl_blend_func(std::uint32_t source_factor, std::uint32_t destination_factor) noexcept
{
    glBlendFunc(static_cast<GLenum>(source_factor), static_cast<GLenum>(destination_factor));
}

void set_opengl_blend_equation_separate(std::uint32_t color_equation, std::uint32_t alpha_equation) noexcept
{
    glBlendEquationSeparate(static_cast<GLenum>(color_equation), static_cast<GLenum>(alpha_equation));
}

void clear_opengl_color(float r, float g, float b, float a) noexcept
{
    glClearColor(r, g, b, a);
}

void clear_opengl_buffers(std::uint32_t mask) noexcept
{
    glClear(static_cast<GLbitfield>(mask));
}

void set_opengl_viewport(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height) noexcept
{
    glViewport(static_cast<GLint>(x), static_cast<GLint>(y), static_cast<GLsizei>(width), static_cast<GLsizei>(height));
}

void set_opengl_scissor(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height) noexcept
{
    glScissor(static_cast<GLint>(x), static_cast<GLint>(y), static_cast<GLsizei>(width), static_cast<GLsizei>(height));
}

void apply_app_viewport(pp::renderer::gl::OpenGlViewportRect viewport)
{
    const auto status = pp::renderer::gl::apply_opengl_viewport(
        viewport,
        pp::renderer::gl::OpenGlViewportDispatch {
            .viewport = 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 = enable_opengl_state,
            .disable = disable_opengl_state,
            .scissor = 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 = enable_opengl_state,
            .disable = disable_opengl_state,
        });
    if (!status.ok())
        LOG("OpenGL scissor test failed: %s", status.message);
}

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

[[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()
{
    width = 1920/2;
    height = 1080/2;
}

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 = clear_opengl_color,
            .clear = clear_opengl_buffers,
        });
    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 (!Settings::load())
        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)
{
    std::function<void(float)> progress = *(std::function<void(float)>*)clientp;
    progress((float)dlnow / (float)dltotal);
    return 0;
}

int progress_callback_upload(void *clientp, curl_off_t dltotal,
    curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow)
{
    std::function<void(float)> progress = *(std::function<void(float)>*)clientp;
    progress((float)ulnow / (float)ultotal);
    return 0;
}
#endif //CURL

void App::download(std::string url, std::string dest_filepath, std::function<void(float)> progress)
{
#if WITH_CURL
    CURL *curl = curl_easy_init();
    if (curl)
    {
        FILE* fp = fopen(dest_filepath.c_str(), "wb");
        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 (disables_network_tls_verification())
            curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
        if (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
    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 (disables_network_tls_verification())
            curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
        if (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::renderer::gl::OpenGlRuntimeInfoDispatch {
                .get_string = query_opengl_string,
            });
        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 = enable_opengl_state,
                .disable = disable_opengl_state,
                .blend_func = set_opengl_blend_func,
                .blend_equation_separate = set_opengl_blend_equation_separate,
            });
        if (!startup_state_status.ok())
            LOG("OpenGL startup state failed: %s", startup_state_status.message);
    });

    const auto startup_plan = pp::app::plan_app_startup(
        Settings::value<Serializer::Integer>("run_counter"),
        Settings::value_or<Serializer::Boolean>("auto-timelapse", true),
        Settings::value_or<Serializer::Boolean>("vr-controllers-enabled", 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);
    }

    initShaders();
    initAssets();
    initLayout();
    title_update();

    uirtt.create(width, height, -1, rgba8_internal_format(), true);

    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()
{
    redraw = true;
    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)
{
    if (n && n->m_display)
    {
        auto box = n->m_clip_uncut;
        Node* p = n->m_parent;
        while (p)
        {
            float pt = YGNodeLayoutGetPadding(p->y_node, YGEdgeTop);
            float pr = YGNodeLayoutGetPadding(p->y_node, YGEdgeRight);
            float pb = YGNodeLayoutGetPadding(p->y_node, YGEdgeBottom);
            float pl = YGNodeLayoutGetPadding(p->y_node, YGEdgeLeft);
            glm::vec2 off_p(pl, pt);
            glm::vec2 off_s(pr, pb);
            //glm::vec2 parent_offset = p->m_parent ? -p->m_parent->m_pos_offset_childred : glm::vec2(0.f);
            glm::vec4 pclip = { xy(p->m_clip_uncut) + off_p, zw(p->m_clip_uncut) - off_s - off_p/* + parent_offset*/ };
            box = rect_intersection(box, pclip);
            p = p->m_parent;
        }
        //auto box = n->m_clip;
        //glm::ivec4 c = glm::vec4((int)box.x - 1, (int)(height / zoom - box.y - box.w) - 1, (int)box.z + 2, (int)box.w + 2) * zoom;
        glm::vec2 parent_offset = n->m_parent ? n->m_parent->m_pos_offset_childred : glm::vec2(0.f);
        if (box.z <= 0 || box.w <= 0)
        {
            if (n->m_on_screen)
            {
                if (dynamic_cast<NodeStrokePreview*>(n))
                    p = p;
                n->handle_on_screen(true, false);
                n->m_on_screen = false;
            }
            return false;
        }
        if (!n->m_on_screen)
        {
            n->handle_on_screen(false, true);
            n->m_on_screen = true;
        }
        glm::ivec4 c = glm::vec4(box.x - 1, (height / zoom - box.y - box.w - 1), box.z + 2, box.w + 2) * zoom;
        apply_app_scissor(pp::renderer::gl::OpenGlScissorRect {
            .enabled = 1U,
            .x = static_cast<std::int32_t>(floorf(c.x + off_x)),
            .y = static_cast<std::int32_t>(floorf(c.y + off_y)),
            .width = static_cast<std::int32_t>(ceilf(c.z)),
            .height = static_cast<std::int32_t>(ceilf(c.w)),
        });
        n->draw();
        return true;
    }
    return false;
}

void App::draw(float dt)
{
    // update offscreen stuff
    if (canvas && canvas->m_canvas)
        canvas->m_canvas->stroke_draw();
    
    auto observer = std::bind(&App::update_ui_observer, this, std::placeholders::_1);

    if (vr_active && ui_visible)
    {
        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 (!vr_only)
    {
        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);
    }

    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);
    
    if (!(redraw || animate))
        return;

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

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

    {
        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");
    ui_save();

    NodeStrokePreview::terminate_renderer();
    rec_stop();
    
    TextureManager::invalidate();
    ShaderManager::invalidate();
    layout.unload();
    layout_designer.unload();
    uirtt.destroy();
    m_face_plane.destroy();
    layers.reset();
    color.reset();
    stroke.reset();
    grid.reset();
    presets.reset();
    floating_presets.reset();
    floating_color.reset();
    floating_layers.reset();
    floating_picker.reset();
    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); }*/);
        if (!rec_running)
            break;

        if (Canvas::I->m_encoder)
        {
            canvas->m_canvas->m_dirty_stroke = false;
            PBO equirect = Canvas::I->m_layers_merge.gen_equirect_pbo(
                Canvas::I->m_encoder->frame_size());
            std::this_thread::yield();
            ImageRef img;
            img.create(equirect.width, equirect.height, equirect.map());
            Canvas::I->m_encoder->encode(img);
            equirect.unmap();
            LOG("rec frame encoded");
            update_rec_frames();
        }
    }
}

void App::render_thread_tick()
{
    static uint32_t count = 0;
    render_thread_id = std::this_thread::get_id();
    render_running = true;
    std::deque<AppTask> working_list;

    // move the task list locally to free the queue for other threads
    {
        std::unique_lock<std::mutex> lock(render_task_mutex);
        if (render_tasklist.empty())
            return;
        working_list = std::move(render_tasklist);
    }

    // execute the tasks
    if (!working_list.empty())
    {
        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 = true;
    while (render_running)
    {
        std::deque<AppTask> working_list;

        // 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; });
            working_list = std::move(render_tasklist);
        }

        // execute the tasks
        if (!working_list.empty())
        {
            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();
    ui_running = true;

    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);
        working_list = std::move(ui_tasklist);
    }

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

    tick(0);

    if (redraw)
    {
        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 = true;

    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;

        update_platform_frame(dt);

        // increment timers
        t_frame += dt;
        t_fps_counter += dt;

        if (t_fps_counter > 1.f)
        {
            report_rendered_frames(rendered_frames);
            t_fps_counter = 0;
            rendered_frames = 0;
        }

        if (platform_enables_live_asset_reloading())
        {
            t_reloader += dt;
            if (t_reloader > 1.0)
            {
                t_reloader = 0;
                if (ShaderManager::reload())
                {
                    stroke->update_controls();
                    redraw = true;
                }
                if (layout.reload())
                    redraw = true;
                if (layout_designer.reload())
                    redraw = true;
            }
        }

        tick(dt);

        if (redraw)
        {
            update(t_frame);
            render_task([this, t_frame]
            {
                bind_default_render_target();
                clear();
                draw(t_frame);
                async_swap();
            });
            t_frame = 0;
            rendered_frames++;
        }
    }
    detach_ui_thread();
}

void App::render_thread_start()
{
    render_thread = std::thread(&App::render_thread_main, this);
    render_running = true;
}

void App::render_thread_stop()
{
    render_running = false;
    render_cv.notify_all();
    if (render_thread.joinable())
        render_thread.join();
}

void App::ui_thread_start()
{
    ui_thread = std::thread(&App::ui_thread_main, this);
    ui_running = true;
}

void App::ui_thread_stop()
{
    ui_running = false;
    ui_cv.notify_all();
    if (ui_thread.joinable())
        ui_thread.join();
}
