#include "pch.h"
#include "log.h"
#include "app.h"
#include "node_icon.h"
#include "node_dialog_open.h"
#include "node_progress_bar.h"

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

#ifdef __ANDROID__
void android_async_lock(struct engine* engine);
void android_async_swap(struct engine* engine);
void android_async_unlock(struct engine* engine);
#elif _WIN32
bool async_lock_try();
void async_lock();
void async_swap();
void async_unlock();
void destroy_window();
#endif

App App::I; // singleton

void App::create()
{
    width = 1920/2;
    height = 1080/2;
}

void App::open_document(std::string path)
{
    std::regex r(R"((.*)[\\/]([^\\/]+)\.(\w+)$)");
    std::smatch m;
    if (!std::regex_search(path, m, r))
        return;
    std::string ext = m[3].str();

    if (str_iequals(m[3].str(), "abr"))
    {
        std::thread(&NodePanelStroke::import_abr, stroke, path).detach();
    }
    else
    {
        doc_name = m[2].str();
        doc_dir = m[1].str();
        doc_path = path;
        canvas->reset_camera();
        layers->clear();
        canvas->m_canvas->project_open(path, [this](bool success){
            // on complete
            if (success)
            {
                async_start();
                title_update();
                for (auto& i : canvas->m_canvas->m_order)
                {
                    auto l = layers->add_layer(canvas->m_canvas->m_layers[i]->m_name.c_str());
                    l->m_visibility->set_value(canvas->m_canvas->m_layers[i]->m_visible);
                }
                async_end();
            }
            else
            {
                message_box("Open Document Error",
                    "There was an error opening the document.\n"
                    "It may be inaccessible or corrupted.");
            }
        });
        ActionManager::clear();
    }
}

bool App::request_close()
{
    static bool dialog_already_opened = false;
    if (!Canvas::I->m_unsaved)
        return true;
    if (!dialog_already_opened)
    {
        async_start();
        auto* m = layout[main_id]->add_child<NodeMessageBox>();
        m->m_title->set_text("Unsaved document");
        m->m_message->set_text("Do you want to close without saving?");
        m->btn_ok->m_text->set_text("Yes");
        m->btn_ok->on_click = [this](Node*) {
#ifdef _WIN32
            destroy_window();
            PostQuitMessage(0);
#endif
#ifdef __OSX__
            [osx_view close];
#endif
            Canvas::I->m_unsaved = false;
        };
        m->btn_cancel->m_text->set_text("No");
        m->btn_cancel->on_click = [this,m](Node*) {
            m->destroy();
            dialog_already_opened = false;
        };
        async_redraw();
        async_end();
        dialog_already_opened = true;
    }
    return false;
}

void App::clear()
{
    glClearColor(.1f, .1f, .1f, 1.f);
    glViewport(off_x, off_y, (GLsizei)width, (GLsizei)height);
    glClear(GL_COLOR_BUFFER_BIT);
}

void App::initAssets()
{
    LOG("initializing assets");
    FontManager::init();
    LOG("initializing assets loading fonts");
    FontManager::load(kFont::Arial_11, "data/fonts/Roboto-Regular.ttf", 17);
    FontManager::load(kFont::Arial_30, "data/fonts/Roboto-Regular.ttf", 30);

    LOG("initializing assets create sampler");
    sampler.create(GL_NEAREST);
    sampler_stencil.create(GL_LINEAR, GL_REPEAT);
    sampler_linear.create(GL_LINEAR);
    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()
{
#if defined(__IOS__)
    NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString* docpath = (NSString*)[paths objectAtIndex:0];
    data_path = [docpath cStringUsingEncoding:NSASCIIStringEncoding];

    NSError* err = nil;

    NSString* recpath = [docpath stringByAppendingString:@"/rec"];
    rec_path = [recpath cStringUsingEncoding:NSASCIIStringEncoding];
    if (![[NSFileManager defaultManager] createDirectoryAtPath:recpath withIntermediateDirectories:YES attributes:nil error:&err])
    {
        LOG("error creating rec path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]);
    }
    // brushes
    if (![[NSFileManager defaultManager] createDirectoryAtPath:[docpath stringByAppendingString:@"/brushes"] withIntermediateDirectories:YES attributes:nil error:&err])
    {
        LOG("error creating brushes path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]);
    }
    if (![[NSFileManager defaultManager] createDirectoryAtPath:[docpath stringByAppendingString:@"/brushes/thumbs"] withIntermediateDirectories:YES attributes:nil error:&err])
    {
        LOG("error creating brushes thumbs path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]);
    }
    // patterns
    if (![[NSFileManager defaultManager] createDirectoryAtPath:[docpath stringByAppendingString:@"/patterns"] withIntermediateDirectories:YES attributes:nil error:&err])
    {
        LOG("error creating patterns path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]);
    }
    if (![[NSFileManager defaultManager] createDirectoryAtPath:[docpath stringByAppendingString:@"/patterns/thumbs"] withIntermediateDirectories:YES attributes:nil error:&err])
    {
        LOG("error creating patterns thumbs path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]);
    }
    // settings
    if (![[NSFileManager defaultManager] createDirectoryAtPath:[docpath stringByAppendingString:@"/settings"] withIntermediateDirectories:YES attributes:nil error:&err])
    {
        LOG("error creating settings path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]);
    }
#elif defined(__OSX__)
    NSArray* paths = NSSearchPathForDirectoriesInDomains(NSPicturesDirectory, NSUserDomainMask, YES);
    NSString* docpath = [(NSString*)[paths objectAtIndex:0] stringByAppendingString:@"/PanoPainter"];

    NSError* err = nil;

    if (![[NSFileManager defaultManager] createDirectoryAtPath:docpath withIntermediateDirectories:YES attributes:nil error:&err])
    {
        LOG("error creating rec path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]);
    }
    data_path = [docpath cStringUsingEncoding:NSASCIIStringEncoding];
    
    NSString* recpath = [docpath stringByAppendingString:@"/rec"];
    rec_path = [recpath cStringUsingEncoding:NSASCIIStringEncoding];
    if (![[NSFileManager defaultManager] createDirectoryAtPath:recpath withIntermediateDirectories:YES attributes:nil error:&err])
    {
        LOG("error creating rec path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]);
    }
    // brushes
    if (![[NSFileManager defaultManager] createDirectoryAtPath:[docpath stringByAppendingString:@"/brushes"] withIntermediateDirectories:YES attributes:nil error:&err])
    {
        LOG("error creating brushes path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]);
    }
    if (![[NSFileManager defaultManager] createDirectoryAtPath:[docpath stringByAppendingString:@"/brushes/thumbs"] withIntermediateDirectories:YES attributes:nil error:&err])
    {
        LOG("error creating brushes thumbs path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]);
    }
    // textures
    if (![[NSFileManager defaultManager] createDirectoryAtPath:[docpath stringByAppendingString:@"/patterns"] withIntermediateDirectories:YES attributes:nil error:&err])
    {
        LOG("error creating patterns path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]);
    }
    if (![[NSFileManager defaultManager] createDirectoryAtPath:[docpath stringByAppendingString:@"/patterns/thumbs"] withIntermediateDirectories:YES attributes:nil error:&err])
    {
        LOG("error creating patterns thumbs path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]);
    }
    // settings
    if (![[NSFileManager defaultManager] createDirectoryAtPath:[docpath stringByAppendingString:@"/settings"] withIntermediateDirectories:YES attributes:nil error:&err])
    {
        LOG("error creating settings path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]);
    }
#elif defined(_WIN32)
    //CHAR my_documents[MAX_PATH];
    //HRESULT result = SHGetFolderPathA(NULL, CSIDL_PERSONAL, NULL, SHGFP_TYPE_CURRENT, my_documents);

    //HMODULE hModule = GetModuleHandle(NULL);
    //CHAR path[MAX_PATH];
    //GetModuleFileNameA(hModule, path, MAX_PATH);
    //CHAR out_drive[MAX_PATH];
    //CHAR out_path[MAX_PATH];
    //_splitpath(path, out_drive, out_path, nullptr, nullptr);
    //sprintf_s(path, "%s%s", out_drive, out_path);
    //data_path = path;


    CHAR my_documents[MAX_PATH];
    HRESULT result = SHGetFolderPathA(NULL, CSIDL_PERSONAL, NULL, SHGFP_TYPE_CURRENT, my_documents);
    if (SUCCEEDED(result))
    {
        std::string path = std::string(my_documents) + "\\PanoPainter";
        if (!PathFileExistsA(path.c_str()))
            CreateDirectoryA(path.c_str(), NULL);
        data_path = path;
    }
    else
    {
        CHAR path[MAX_PATH];
        GetCurrentDirectoryA(sizeof(path), path);
        data_path = path;
    }

    rec_path = data_path + "\\frames";
    if (!PathFileExistsA(rec_path.c_str()))
        CreateDirectoryA(rec_path.c_str(), NULL);

    if (!PathFileExistsA((data_path + "\\brushes").c_str()))
        CreateDirectoryA((data_path + "\\brushes").c_str(), NULL);
    if (!PathFileExistsA((data_path + "\\brushes\\thumbs").c_str()))
        CreateDirectoryA((data_path + "\\brushes\\thumbs").c_str(), NULL);

    if (!PathFileExistsA((data_path + "\\patterns").c_str()))
        CreateDirectoryA((data_path + "\\patterns").c_str(), NULL);
    if (!PathFileExistsA((data_path + "\\patterns\\thumbs").c_str()))
        CreateDirectoryA((data_path + "\\patterns\\thumbs").c_str(), NULL);

    if (!PathFileExistsA((data_path + "\\settings").c_str()))
        CreateDirectoryA((data_path + "\\settings").c_str(), NULL);
#endif

    // TODO: save this path somewhere in the settings, don't overwrite every start
    work_path = data_path;

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

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;
}

void App::download(std::string url, std::string dest_filepath, std::function<void(float)> progress)
{
    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);
#ifdef __ANDROID__
        curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
#endif
        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);
    }
}

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

    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);
#ifdef __ANDROID__
        curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
#endif

        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;
    }
    return false;
}

void App::upload(std::string filename, std::string name, std::function<void(float)> progress)
{
    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);
#ifdef __ANDROID__
        curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
#endif
        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);
    }
}

void App::init()
{
#ifdef _WIN32
    static CONSOLE_SCREEN_BUFFER_INFO info;
    GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info);
    // colors: http://stackoverflow.com/questions/4053837/colorizing-text-in-the-console-with-c
    glDebugMessageCallback([](GLenum source, GLenum type, GLuint id,
        GLenum severity, GLsizei length, const GLchar* message, const void* userParam) 
    {
        static std::map<GLenum, int> colors = {
            { GL_DEBUG_SEVERITY_NOTIFICATION, 8 },
            { GL_DEBUG_SEVERITY_LOW, 8 },
            { GL_DEBUG_SEVERITY_MEDIUM, FOREGROUND_GREEN | FOREGROUND_INTENSITY },
            { GL_DEBUG_SEVERITY_HIGH, FOREGROUND_RED | FOREGROUND_INTENSITY },
        };
        if (severity == GL_DEBUG_SEVERITY_HIGH || severity == GL_DEBUG_SEVERITY_MEDIUM || severity == GL_DEBUG_SEVERITY_LOW)
        {
            SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), colors[severity]);
            LOG("OPENGL: %.*s", length, message);
            FlushConsoleInputBuffer(GetStdHandle(STD_OUTPUT_HANDLE));
            SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), info.wAttributes);
#ifdef _WIN32
            if (severity == GL_DEBUG_SEVERITY_HIGH)
                __debugbreak();
#endif
        }
    }, nullptr);
    glEnable(GL_DEBUG_OUTPUT);
    glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
#endif

    LOG("GL version: %s", glGetString(GL_VERSION));
    LOG("GL vendor: %s", glGetString(GL_VENDOR));
    LOG("GL renderer: %s", glGetString(GL_RENDERER));

    //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));
    //    }
    //}

    LOG("Screen Resolution: %dx%d", (int)width, (int)height);

    //zoom = ceilf(width / 2000.f);
    //zoom = 2;
    
    glDisable(GL_DEPTH_TEST);
#if defined(_WIN32) || defined(__OSX__)
    glEnable(GL_PROGRAM_POINT_SIZE);
    glEnable(GL_LINE_SMOOTH);
#endif
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    glBlendEquationSeparate(GL_FUNC_ADD, GL_MAX);

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

    if (!check_license())
    {
        message_box("License", "Could not validate this license, running in demo mode.");
    }
    
    //GLfloat width_range[2];
    //glGetFloatv(GL_SMOOTH_LINE_WIDTH_RANGE, width_range);
    //LOG("GL line range: %f - %f", width_range[0], width_range[1]);
    LOG("Screen Size: %f %f", width, height);
    
    GLint fb0;
    glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &fb0);
    LOG("Default Framebuffer %d", fb0);
}

void App::async_start()
{
#if __OSX__
    [osx_view async_lock];
#elif __IOS__
    [ios_view async_lock];
#elif __ANDROID__
    android_async_lock(and_engine);
#elif _WIN32
    async_lock();
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
#endif
}

void App::async_update()
{
#if __IOS__
    [ios_view->glview bindDrawable];
#elif _WIN32
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
#endif
    redraw = true;
    clear();
    update(0);
#if __OSX__
    [osx_view async_swap];
#elif __IOS__
    [ios_view->glview bindDrawable];
    [ios_view async_swap];
#elif __ANDROID__
    android_async_swap(and_engine);
#elif _WIN32
    async_swap();
#endif
}

void App::async_redraw()
{
    redraw = true;
}

void App::async_end()
{
#if __OSX__
    [osx_view async_unlock];
#elif __IOS__
    [ios_view async_unlock];
#elif __ANDROID__
    android_async_unlock(and_engine);
#elif _WIN32
    async_unlock();
#endif
}

void App::update(float dt)
{
    static std::mutex mutex;
    static float rec_timer = 0.f;
    static float reload_timer = 0.f;

    // avoid multiple threads to update the scene
    std::lock_guard<std::mutex> lock(mutex);
    
    static std::mutex m;
    std::lock_guard<std::mutex> _lock(m);

    // update offscreen stuff
    if (canvas && canvas->m_canvas)
        canvas->m_canvas->stroke_draw();
    
    if (!(redraw || animate))
        return;

    //glClearColor(.1f, .1f, .1f, 1.f);
    //glViewport(0, 0, (GLsizei)width, (GLsizei)height);
    //glClear(GL_COLOR_BUFFER_BIT);

    //if (!canvas->m_mouse_captured)
#if _DEBUG && (_WIN32 || __OSX__)
    reload_timer += dt;
    if (reload_timer > 1.0)
    {
        reload_timer = 0;
        if (ShaderManager::reload())
            stroke->update_controls();
        layout.reload();
    }
#endif

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

    {
        static glm::vec4 color_button_normal{ .1, .1, .1, 1 };
        static glm::vec4 color_button_hlight{ 1, .0, .0, 1 };

        auto mode = Canvas::I->m_current_mode;

        CanvasModePen* pm = (CanvasModePen*)canvas->m_canvas->modes[(int)kCanvasMode::Draw][0];
        layout[main_id]->find<NodeButtonCustom>("btn-pick")->set_active(mode == kCanvasMode::Draw && pm->m_picking);
        layout[main_id]->find<NodeButtonCustom>("btn-touchlock")->set_active(canvas->m_canvas->m_touch_lock);

        layout[main_id]->find<NodeButtonCustom>("btn-pen")->set_active(mode == kCanvasMode::Draw);
        layout[main_id]->find<NodeButtonCustom>("btn-erase")->set_active(mode == kCanvasMode::Erase);
        layout[main_id]->find<NodeButton>("btn-cam")->set_active(mode == kCanvasMode::Camera);
        layout[main_id]->find<NodeButtonCustom>("btn-line")->set_active(mode == kCanvasMode::Line);
        layout[main_id]->find<NodeButton>("btn-grid")->set_active(mode == kCanvasMode::Grid);
        layout[main_id]->find<NodeButton>("btn-copy")->set_active(mode == kCanvasMode::Copy);
        layout[main_id]->find<NodeButton>("btn-cut")->set_active(mode == kCanvasMode::Cut);
        layout[main_id]->find<NodeButtonCustom>("btn-mask-free")->set_active(mode == kCanvasMode::MaskFree);
        layout[main_id]->find<NodeButtonCustom>("btn-mask-line")->set_active(mode == kCanvasMode::MaskLine);
    }

    auto observer = [this](Node* n)
    {
        if (n && n->m_display)
        {
            auto box = n->m_clip_uncut;
            Node* p = n->parent;
            if (dynamic_cast<NodeBrushPresetItem*>(n))
                p = p;
            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::vec4 pclip = { xy(p->m_clip_uncut) + off_p, zw(p->m_clip_uncut) - off_s - off_p };
                box = rect_intersection(box, pclip);
                p = p->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::ivec4 c = glm::vec4((int)box.x, (int)(height / zoom - box.y - box.w), (int)box.z, (int)box.w) * zoom;
            glScissor(c.x + off_x, c.y + off_y, c.z, c.w);
            n->draw();
            return true;
        }
        return false;
    };
    
    if (vr_active)
    {
        uirtt.bindFramebuffer();
        uirtt.clear();
        glEnable(GL_SCISSOR_TEST);
        for (int i = 1; i < layout[main_id]->m_children.size(); i++)
            layout[main_id]->m_children[i]->watch(observer);
        //msgbox->watch(observer);
        glDisable(GL_SCISSOR_TEST);
        uirtt.unbindFramebuffer();
    }
    
#if __IOS__
    [ios_view->glview bindDrawable];
#else
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
#endif
    //canvas->watch(observer);
    glEnable(GL_SCISSOR_TEST);
    for (int i = 0; i < layout[main_id]->m_children.size(); i++)
        layout[main_id]->m_children[i]->watch(observer);
    //msgbox->watch(observer);
    glDisable(GL_SCISSOR_TEST);

//     glEnable(GL_BLEND);
//     sampler.bind(0);
//     ShaderManager::use(kShader::Texture);
//     ShaderManager::u_int(kShaderUniform::Tex, 0);
//     ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-1.f, 1.f, -1.f, 1.f));
//     glActiveTexture(GL_TEXTURE0);
//     uirtt.bindTexture();
//     m_face_plane.draw_fill();
//     uirtt.unbindTexture();
//     glDisable(GL_BLEND);

    if (rec_running)
    {
        rec_timer += dt;
        if (rec_timer > 1.f && canvas->m_canvas->m_dirty_stroke)
        {
            canvas->m_canvas->m_dirty_stroke = false;
            
            LOG("rec tick");
            rec_timer = 0.f;
            
            auto data = new uint8_t[width * height * 4];
            #if __IOS__
                [ios_view->glview bindDrawable];
            #else
                glBindFramebuffer(GL_FRAMEBUFFER, 0);
            #endif
            
            GLint dfbo, rfbo;
            glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &rfbo);
            glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &dfbo);
            if (dfbo != rfbo)
                LOG("DIFFERENT FB");

            glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, data);
            {
                std::lock_guard<std::mutex> lock(rec_mutex);
                rec_frames.emplace_back(data);
                rec_cv.notify_all();
            }
            update_rec_frames();
        }
    }
    
    redraw = false;
}

void App::terminate()
{
    LOG("App::terminate");
    TextureManager::invalidate();
    ShaderManager::invalidate();
    NodeStrokePreview::terminate_renderer();
    layout.unload();
    uirtt.destroy();
    m_face_plane.destroy();
    layers.reset();
    color.reset();
    stroke.reset();
    grid.reset();
    rec_stop();
}

void App::update_memory_usage(size_t bytes)
{
    if (auto txt = layout[main_id]->find<NodeText>("txt-memory"))
    {
        static char buffer[128];
        sprintf(buffer, "History memory: %.2f Mb", bytes / 1024.f / 1024.f);
        txt->set_text(buffer);
        layout[main_id]->update();
    }
}

void App::update_rec_frames()
{
    if (auto txt = layout[main_id]->find<NodeText>("txt-rec"))
    {
        if (rec_running)
        {
            static char buffer[128];
            sprintf(buffer, "Recorder %d frames", rec_count);
            txt->set_text(buffer);
        }
        else
        {
            txt->set_text("");
        }
        layout[main_id]->update();
    }
}

int App::res_from_index(int i)
{
    return res_map[i];
}

int App::res_to_index(int res)
{
    return std::distance(res_map.begin(), std::find(res_map.begin(), res_map.end(), res));
}

std::string App::res_to_string(int res)
{
    return res_map_str[res_to_index(res)];
}

void App::rec_clear()
{
    rec_stop();
#if defined(__IOS__) || defined(__OSX__)
    NSString *path = [NSString stringWithUTF8String:rec_path.c_str()];
    NSDirectoryEnumerator* en = [[NSFileManager defaultManager] enumeratorAtPath:path];
    NSError* err = nil;
    BOOL res;
    NSString* file;
    while (file = [en nextObject])
    {
        NSString* file_path = [path stringByAppendingPathComponent:file];
        [[NSFileManager defaultManager] removeItemAtPath:file_path error:nil];
        NSLog(@"delete: %@", file_path);
    }
#endif
    rec_count = 0;
    update_rec_frames();
}

void App::rec_start()
{
    if (!rec_running)
    {
#if defined(__IOS__) || defined(__OSX__)
        NSString* path = [NSString stringWithUTF8String:rec_path.c_str()];
        NSArray *dirFiles = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:nil];
        NSArray *jpgFiles = [dirFiles filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"self ENDSWITH '.jpg'"]];
        rec_count = (int)[jpgFiles count];
        update_rec_frames();
        rec_thread = std::thread(&App::rec_loop, this);
#else
        rec_count = Asset::list_files(rec_path, false, ".*\\.jpg").size();
        update_rec_frames();
        rec_thread = std::thread(&App::rec_loop, this);
#endif
    }
}

void App::rec_stop()
{
    if (rec_running)
    {
        rec_running = false;
        rec_cv.notify_all();
        if (rec_thread.joinable())
            rec_thread.join();
        update_rec_frames();
    }
}

void stillImageDataReleaseCallback(void *releaseRefCon, const void *baseAddress)
{
    free((void *)baseAddress);
}

void App::rec_export(std::string path)
{
    int progress = 0;
    int tot = rec_count;
    auto pb = layout[main_id]->add_child<NodeProgressBar>();
    pb->m_progress->SetWidthP(0);
    pb->m_title->set_text("Exporting MP4 movie");
    async_update();

#if defined(__IOS__) || defined(__OSX__)
    NSString* mov_path = [NSString stringWithFormat:@"%s/out.mp4", rec_path.c_str()];
    if ([[NSFileManager defaultManager] fileExistsAtPath:mov_path])
    {
        NSLog(@"remove existing mp4");
        [[NSFileManager defaultManager] removeItemAtPath:mov_path error:nil];
    }
    NSURL* url = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%s/out.mp4", rec_path.c_str()]];
    AVAssetWriter* writer = [AVAssetWriter assetWriterWithURL:url fileType:AVFileTypeMPEG4 error:nil];
    writer.shouldOptimizeForNetworkUse = NO;
    NSDictionary *videoCompressionSettings = @{
        AVVideoCodecKey                  : AVVideoCodecH264,
        AVVideoWidthKey                  : @(width),
        AVVideoHeightKey                 : @(height),
        AVVideoCompressionPropertiesKey  : @{ AVVideoAverageBitRateKey : @(8<<20) }
    };
    if (![writer canApplyOutputSettings:videoCompressionSettings forMediaType:AVMediaTypeVideo])
    {
        NSLog(@"Couldn't add asset writer video input.");
        return;
    }
    AVAssetWriterInput* input = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo
                                                              outputSettings:videoCompressionSettings
                                                            sourceFormatHint:nil];
    input.expectsMediaDataInRealTime = YES;
  
    NSDictionary *adaptorDict = @{
                                (id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA),
                                (id)kCVPixelBufferWidthKey : @(width),
                                (id)kCVPixelBufferHeightKey : @(height)
                                };

    AVAssetWriterInputPixelBufferAdaptor* _pixelBufferAdaptor = [[AVAssetWriterInputPixelBufferAdaptor alloc]
                         initWithAssetWriterInput:input
                         sourcePixelBufferAttributes:adaptorDict];


    // Add asset writer input to asset writer
    if (![writer canAddInput:input]) {
        NSLog(@"Couldn't add input to writer.");
        return;
    }

    [writer addInput:input];

    CMTime t;
    t.timescale = 30;
    t.flags = kCMTimeFlags_Valid;
    t.epoch = 0;
    t.value = 0;
    //[writer startSessionAtSourceTime:t];
    
    CVPixelBufferRef buff = NULL;
    uint8_t* data = (uint8_t*)calloc(1, width * height * 4);
    CVPixelBufferCreateWithBytes(kCFAllocatorDefault, width, height, kCVPixelFormatType_32RGBA, data,
        width * 4, stillImageDataReleaseCallback, nil, nil, &buff);
    OSStatus err = CVPixelBufferPoolCreatePixelBuffer(nil, _pixelBufferAdaptor.pixelBufferPool, &buff);


    if (writer.status == AVAssetWriterStatusUnknown)
    {
        // If the asset writer status is unknown, implies writing hasn't started yet, hence start writing with start time as the buffer's presentation timestamp
        if ([writer startWriting])
        {
            [writer startSessionAtSourceTime:t];
        }
    }

    if (writer.status == AVAssetWriterStatusWriting)
    {
        for (int i = 0; i < tot; i++)
        {
            // If the asset writer status is writing, append sample buffer to its corresponding asset writer input
            if (input.readyForMoreMediaData)
            {
                char path[256];
                snprintf(path, sizeof(path), "%s/%04d.jpg", rec_path.c_str(), i);
                NSString* img_path = [NSString stringWithUTF8String:path];
                NSLog(@"frame: %@", img_path);
#if __OSX__
                NSImage *image = [[NSImage alloc] initWithContentsOfFile:img_path];
                if (!image)
                    break;
                NSRect imageRect = NSMakeRect(0, 0, image.size.width, image.size.height);
                CGImageRef cgImage = [image CGImageForProposedRect:&imageRect context:NULL hints:nil];
#elif __IOS__
                UIImage* image = [UIImage imageNamed:img_path];
                if (!image)
                    break;
                CGImageRef cgImage = image.CGImage;
#endif

                NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
                             [NSNumber numberWithBool:YES], kCVPixelBufferCGImageCompatibilityKey,
                             [NSNumber numberWithBool:YES], kCVPixelBufferCGBitmapContextCompatibilityKey, nil];
                CGSize sz = image.size;
                CVPixelBufferRef pxbuffer = NULL;
                {
                    CGImageRef image = cgImage;
                    CGSize size = sz;
                    CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, size.width, size.height, kCVPixelFormatType_32ARGB, (__bridge CFDictionaryRef) options, &pxbuffer);
                    // CVReturn status = CVPixelBufferPoolCreatePixelBuffer(NULL, adaptor.pixelBufferPool, &pxbuffer);

                    //NSParameterAssert(status == kCVReturnSuccess && pxbuffer != NULL);

                    CVPixelBufferLockBaseAddress(pxbuffer, 0);
                    void *pxdata = CVPixelBufferGetBaseAddress(pxbuffer);
                    //NSParameterAssert(pxdata != NULL);

                    CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
                    CGContextRef context = CGBitmapContextCreate(pxdata, size.width, size.height, 8, 4*size.width, rgbColorSpace, kCGImageAlphaPremultipliedFirst);
                    //NSParameterAssert(context);

                    CGContextDrawImage(context, CGRectMake(0, 0, CGImageGetWidth(image), CGImageGetHeight(image)), image);

                    CGColorSpaceRelease(rgbColorSpace);
                    CGContextRelease(context);

                    CVPixelBufferUnlockBaseAddress(pxbuffer, 0);
                }

                t.value = i;
                if (![_pixelBufferAdaptor appendPixelBuffer:pxbuffer withPresentationTime:t])
                {
                    NSLog(@"error %@", [writer.error localizedFailureReason]);
                }
                CFRelease(pxbuffer);
                
                progress++;
                pb->m_progress->SetWidthP((float)progress / tot * 100.f);
                async_update();
            }
        }
        [input markAsFinished];
        [writer finishWritingWithCompletionHandler:^{
            NSString* path = [NSString stringWithFormat:@"%s/out.mp4", rec_path.c_str()];
            NSLog(@"saved video %@", path);
#if __IOS__
            if (UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(path))
            {
                NSLog(@"saving to camera roll");
                UISaveVideoAtPathToSavedPhotosAlbum(path, nil, nil, nil);
            }
#endif
        }];
    }

    if (writer.status == AVAssetWriterStatusFailed)
    {
        NSLog(@"failed");
    }
    
#endif
    
    pb->destroy();
    async_update();

}

void App::rec_loop()
{
    rec_running = true;
    while(rec_running)
    {
        std::unique_lock<std::mutex> lock(rec_mutex);
        rec_cv.wait(lock);
        if (!rec_running)
            break;
        if (!rec_frames.empty())
        {
            if (rec_frames.front())
            {
                auto inverted = std::make_unique<uint8_t[]>(width*height*4);
                for (int y = height - 1, y1 = 0; y >= 0; y--, y1++)
                {
                    uint8_t* dst = &inverted[y * width * 4];
                    uint8_t* src = &rec_frames.front()[y1 * width * 4];
                    std::copy_n(src, (int)width * 4, dst);
                }
                char path[256];
                snprintf(path, sizeof(path), "%s/%04d.jpg", rec_path.c_str(), rec_count);
                LOG("writing %s", path);
                jpge::params params;
                params.m_quality = 75;
                bool saved = jpge::compress_image_to_jpeg_file(path, width, height, 4, inverted.get(), params);
                if (!saved)
                {
                    LOG("error writing the frame");
                    rec_running = false;
                }
                else
                {
                    rec_count++;
                    redraw = true;
                }
            }
            rec_frames.pop_front();
        }
    }
}
