#include "pch.h"

#include <algorithm>
#include <array>
#include <cstdint>
#include <memory>
#include <vector>

#include "app_core/canvas_hotkey.h"
#include "app_core/canvas_tool_ui.h"
#include "app.h"
#include "legacy_canvas_tool_services.h"
#include "legacy_history_services.h"
#include "log.h"
#include "node_canvas.h"
#include "node_image_texture.h"
#include "paint_renderer/compositor.h"
#include "settings.h"
#include "renderer_gl/opengl_capabilities.h"
#include "util.h"

namespace {

void activate_opengl_texture(std::uint32_t texture_unit) noexcept
{
    glActiveTexture(static_cast<GLenum>(texture_unit));
}

void bind_opengl_texture(std::uint32_t target, std::uint32_t texture) noexcept
{
    glBindTexture(static_cast<GLenum>(target), static_cast<GLuint>(texture));
}

void set_active_texture_unit(std::uint32_t unit_index)
{
    const auto status = pp::renderer::gl::activate_opengl_texture_unit(
        unit_index,
        pp::renderer::gl::OpenGlActiveTextureDispatch {
            .active_texture = activate_opengl_texture,
        });
    if (!status.ok())
        LOG("NodeCanvas active texture dispatch failed because: %s", status.message);
}

void unbind_texture_2d()
{
    const auto status = pp::renderer::gl::bind_opengl_texture_2d(
        0U,
        pp::renderer::gl::OpenGlTexture2DBindDispatch {
            .bind_texture = bind_opengl_texture,
        });
    if (!status.ok())
        LOG("NodeCanvas texture unbind dispatch failed because: %s", status.message);
}

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

void disable_opengl_state(std::uint32_t state) noexcept
{
    glDisable(static_cast<GLenum>(state));
}

std::uint8_t is_opengl_state_enabled(std::uint32_t state) noexcept
{
    return static_cast<std::uint8_t>(glIsEnabled(static_cast<GLenum>(state)));
}

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_clear_color(float r, float g, float b, float a) noexcept
{
    glClearColor(r, g, b, a);
}

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

void get_opengl_integer(std::uint32_t name, std::int32_t* values) noexcept
{
    GLint raw_values[4] {};
    glGetIntegerv(static_cast<GLenum>(name), raw_values);
    values[0] = static_cast<std::int32_t>(raw_values[0]);
    values[1] = static_cast<std::int32_t>(raw_values[1]);
    values[2] = static_cast<std::int32_t>(raw_values[2]);
    values[3] = static_cast<std::int32_t>(raw_values[3]);
}

void get_opengl_float(std::uint32_t name, float* values) noexcept
{
    glGetFloatv(static_cast<GLenum>(name), values);
}

void apply_node_canvas_viewport(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height)
{
    const auto status = pp::renderer::gl::apply_opengl_viewport(
        pp::renderer::gl::OpenGlViewportRect {
            .x = x,
            .y = y,
            .width = width,
            .height = height,
        },
        pp::renderer::gl::OpenGlViewportDispatch {
            .viewport = set_opengl_viewport,
        });
    if (!status.ok())
        LOG("NodeCanvas viewport dispatch failed because: %s", status.message);
}

pp::renderer::gl::OpenGlViewportRect query_node_canvas_viewport()
{
    const auto result = pp::renderer::gl::query_opengl_viewport(
        pp::renderer::gl::OpenGlViewportQueryDispatch {
            .get_integer = get_opengl_integer,
        });
    if (!result.ok()) {
        LOG("NodeCanvas viewport query dispatch failed because: %s", result.status().message);
    }
    return result.value();
}

std::array<float, 4> query_node_canvas_clear_color()
{
    const auto result = pp::renderer::gl::query_opengl_clear_color(
        pp::renderer::gl::OpenGlClearColorQueryDispatch {
            .get_float = get_opengl_float,
        });
    if (!result.ok()) {
        LOG("NodeCanvas clear-color query dispatch failed because: %s", result.status().message);
    }
    return result.value();
}

void apply_node_canvas_clear_color(std::array<float, 4> color)
{
    const auto status = pp::renderer::gl::apply_opengl_clear_color(
        color,
        pp::renderer::gl::OpenGlClearColorDispatch {
            .clear_color = set_opengl_clear_color,
        });
    if (!status.ok())
        LOG("NodeCanvas clear-color dispatch failed because: %s", status.message);
}

void clear_node_canvas_color_buffer(std::array<float, 4> color)
{
    const auto status = pp::renderer::gl::clear_opengl_render_target(
        pp::renderer::gl::OpenGlDefaultClear {
            .color = color,
            .mask = pp::renderer::gl::framebuffer_color_buffer_mask(),
        },
        pp::renderer::gl::OpenGlClearDispatch {
            .clear_color = set_opengl_clear_color,
            .clear = clear_opengl_buffer,
        });
    if (!status.ok())
        LOG("NodeCanvas color-buffer clear dispatch failed because: %s", status.message);
}

void apply_node_canvas_capability(std::uint32_t state, bool enabled)
{
    const auto status = pp::renderer::gl::apply_opengl_capability(
        state,
        enabled,
        pp::renderer::gl::OpenGlCapabilityDispatch {
            .enable = enable_opengl_state,
            .disable = disable_opengl_state,
        });
    if (!status.ok())
        LOG("NodeCanvas capability dispatch failed because: %s", status.message);
}

bool query_node_canvas_capability(std::uint32_t state)
{
    const auto result = pp::renderer::gl::query_opengl_capability_state(
        state,
        pp::renderer::gl::OpenGlCapabilityStateQueryDispatch {
            .is_enabled = is_opengl_state_enabled,
        });
    if (!result.ok()) {
        LOG("NodeCanvas capability query failed because: %s", result.status().message);
        return false;
    }
    return result.value();
}

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

pp::paint_renderer::CanvasBlendGatePlan node_canvas_blend_gate_plan(
    int width,
    int height,
    const std::vector<std::shared_ptr<Layer>>& layers,
    const Brush* brush) noexcept
{
    std::vector<int> layer_blend_modes;
    layer_blend_modes.reserve(layers.size());
    for (const auto& layer : layers) {
        if (!layer) {
            continue;
        }
        layer_blend_modes.push_back(layer->m_blend_mode);
    }

    const auto plan = pp::paint_renderer::plan_canvas_blend_gate(
        node_canvas_stroke_composite_features(),
        pp::paint_renderer::CanvasBlendGateRequest {
            .extent = pp::renderer::Extent2D {
                .width = static_cast<std::uint32_t>(std::max(width, 0)),
                .height = static_cast<std::uint32_t>(std::max(height, 0)),
            },
            .layer_blend_modes = layer_blend_modes,
            .has_stroke_blend_mode = brush != nullptr,
            .stroke_blend_mode = brush ? brush->m_blend_mode : 0,
        });
    if (plan) {
        return plan.value();
    }

    pp::paint_renderer::CanvasBlendGatePlan fallback;
    fallback.shader_blend = true;
    fallback.complex_blend = true;
    fallback.compatibility_fallback = true;
    return fallback;
}

pp::app::CanvasHotkeyKey canvas_hotkey_key(kKey key) noexcept
{
    switch (key) {
    case kKey::AndroidBack:
        return pp::app::CanvasHotkeyKey::android_back;
    case kKey::KeyAlt:
        return pp::app::CanvasHotkeyKey::alt;
    case kKey::KeyE:
        return pp::app::CanvasHotkeyKey::e;
    case kKey::KeyS:
        return pp::app::CanvasHotkeyKey::s;
    case kKey::KeyTab:
        return pp::app::CanvasHotkeyKey::tab;
    case kKey::KeyZ:
        return pp::app::CanvasHotkeyKey::z;
    case kKey::KeyBracketLeft:
        return pp::app::CanvasHotkeyKey::bracket_left;
    case kKey::KeyBracketRight:
        return pp::app::CanvasHotkeyKey::bracket_right;
    default:
        return pp::app::CanvasHotkeyKey::other;
    }
}

pp::app::CanvasHotkeyState canvas_hotkey_state(bool mouse_focused, int touch_finger_count = 0) noexcept
{
    pp::app::CanvasHotkeyState state;
    state.ctrl_down = App::I && App::I->keys[(int)kKey::KeyCtrl];
    state.shift_down = App::I && App::I->keys[(int)kKey::KeyShift];
    state.mouse_focused = mouse_focused;
    const auto history = pp::panopainter::legacy_history_snapshot();
    state.undo_count = history.undo_count;
    state.redo_count = history.redo_count;
    state.touch_finger_count = touch_finger_count;
    return state;
}

void execute_canvas_hotkey_plan(const pp::app::CanvasHotkeyPlan& plan)
{
    const auto status = pp::panopainter::execute_legacy_canvas_hotkey_plan(plan);
    if (!status.ok())
        LOG("Canvas hotkey action failed: %s", status.message);
}

void run_canvas_hotkey(
    pp::app::CanvasHotkeyEvent event,
    kKey key,
    bool mouse_focused,
    int touch_finger_count = 0)
{
    const auto plan = pp::app::plan_canvas_hotkey(
        event,
        canvas_hotkey_key(key),
        canvas_hotkey_state(mouse_focused, touch_finger_count));
    if (plan)
        execute_canvas_hotkey_plan(plan.value());
    else
        LOG("Canvas hotkey planning failed: %s", plan.status().message);
}

void run_canvas_tool_mode(pp::app::CanvasToolMode mode)
{
    const auto plan = pp::app::plan_canvas_tool_select(mode);
    const auto status = pp::panopainter::execute_legacy_canvas_input_tool_plan(plan);
    if (!status.ok())
        LOG("Canvas input tool action failed: %s", status.message);
}

}

Node* NodeCanvas::clone_instantiate() const
{
    return new NodeCanvas();
}

void NodeCanvas::init()
{
    m_density = Settings::value_or<Serializer::Float>("vp-scale", 1.f);
    m_cursor_visibility = (kCursorVisibility)Settings::value_or<Serializer::Integer>("show-cursor", 0);

    m_mouse_ignore = false;
    m_canvas = std::make_unique<Canvas>();
    const int canvas_resolution = App::I->default_canvas_resolution();
    m_canvas->create(canvas_resolution, canvas_resolution);
    m_canvas->m_unsaved = false;
    m_canvas->m_node = this;
    
    m_sampler.create();
    //m_sampler.set_filter(pp::renderer::gl::linear_texture_filter(), pp::renderer::gl::nearest_texture_filter());
    
    m_sampler_nearest.create(pp::renderer::gl::nearest_texture_filter());
    
    m_sampler_linear.create(pp::renderer::gl::linear_texture_filter());
    m_sampler_stencil.create(
        pp::renderer::gl::linear_texture_filter(),
        pp::renderer::gl::repeat_texture_wrap());
    m_face_plane.create<1>(2, 2);
    m_line.create();
    CanvasMode::node = this;
    for (int i = 0; i < (int)kCanvasMode::COUNT; i++)
        for (auto m : Canvas::modes[i])
            m->init();

    m_grid.create(1, 1, m_grid_divs);
}

void NodeCanvas::restore_context()
{
    Node::restore_context();
    const int canvas_resolution = App::I->default_canvas_resolution();
    m_canvas->create(canvas_resolution, canvas_resolution);

    m_sampler.create();

    m_sampler.set_filter(
        pp::renderer::gl::linear_texture_filter(),
        pp::renderer::gl::nearest_texture_filter());
    m_face_plane.create<1>(2, 2);
    m_canvas->snapshot_restore();
    CanvasMode::node = this;
    for (int i = 0; i < (int)kCanvasMode::COUNT; i++)
        for (auto m : Canvas::modes[i])
            m->init();
}

void NodeCanvas::clear_context()
{
    Node::clear_context();
    m_canvas->snapshot_save();
    m_canvas->clear_context();
    // TODO: clear CanvasMode objects
}

void NodeCanvas::draw()
{
    // sanity checks
    float zoom = root()->m_zoom;
    if (zoom == 0.f)
        zoom = 1.f;
    auto box = m_clip * zoom;
    if (box.z == 0 || box.w == 0)
        return;

    const auto vp = query_node_canvas_viewport();
    const auto cc = query_node_canvas_clear_color();
    const auto blend = query_node_canvas_capability(pp::renderer::gl::blend_state());
    const auto depth = query_node_canvas_capability(pp::renderer::gl::depth_test_state());
    const auto scissor = query_node_canvas_capability(pp::renderer::gl::scissor_test_state());

    apply_node_canvas_capability(pp::renderer::gl::scissor_test_state(), false);

    glm::ivec4 c = (glm::ivec4)glm::vec4(box.x, (int)(vp.height - box.y - box.w), box.z, box.w);

    //m_canvas->m_cam_rot = m_pan * 0.003f;

    glm::mat4 ortho_proj = glm::ortho(0.f, box.z, 0.f, box.w, -1000.f, 1000.f);
    glm::mat4 proj = glm::perspective(glm::radians(m_canvas->m_cam_fov), box.z / box.w, 0.001f, 1000.f);
    glm::mat4 camera = glm::translate(m_canvas->m_cam_pos) * m_canvas->m_cam_rot;

    m_canvas->m_mv = camera;
    m_canvas->m_proj = proj;
    m_canvas->m_box = box;
    m_canvas->m_vp = c;

    float pitch = 0;
    if (auto slider = root()->find<NodeSliderH>("pitch-slider"))
        pitch = (slider->get_value() - 0.5) * glm::half_pi<float>();
    float yaw = 0;
    if (auto slider = root()->find<NodeSliderH>("yaw-slider"))
        yaw = (slider->get_value() - 0.5) * glm::half_pi<float>();
    float roll = 0;
    if (auto slider = root()->find<NodeSliderH>("roll-slider"))
        roll = (slider->get_value() - 0.5) * glm::half_pi<float>();

    // pre computed helpers
    for (int plane_index = 0; plane_index < 6; plane_index++)
    {
        //glm::mat4 plane_camera = glm::lookAt(m_canvas->m_plane_origin[plane_index], m_canvas->m_plane_normal[plane_index], m_canvas->m_plane_tangent[plane_index]);
        m_canvas->m_plane_unproject[plane_index] = glm::inverse(m_canvas->m_proj * m_canvas->m_mv * m_canvas->m_plane_transform[plane_index]);
        m_canvas->m_plane_dir[plane_index] = -(m_canvas->m_plane_transform[plane_index] * glm::vec4(m_canvas->m_plane_origin[plane_index], 1));
        // face is the 2d shape of the cube plane i projected onto the window space
        m_canvas->m_plane_shape[plane_index] = m_canvas->face_to_shape2D(plane_index);
    }


    if (m_density != 1.f)
    {
        m_rtt.bindFramebuffer();
        clear_node_canvas_color_buffer({ 1.f, 1.f, 0.f, 0.f });
        apply_node_canvas_viewport(0, 0, m_rtt.getWidth(), m_rtt.getHeight());
    }
    else
    {
        clear_node_canvas_color_buffer({ 1.f, 1.f, 1.f, 0.f });
        apply_node_canvas_viewport(c.x + App::I->off_x, c.y + App::I->off_y, c.z, c.w);
    }

    // NOTE: draw_merge has been disabled for worst performance
    bool draw_merged = !(m_canvas->m_current_mode == kCanvasMode::Camera);
    draw_merged = false;

    if (draw_merged)
    {
        apply_node_canvas_capability(pp::renderer::gl::blend_state(), false);
        // draw the grid
        for (int plane_index = 0; plane_index < 6; plane_index++)
        {
            auto plane_mvp = proj * camera *
                glm::scale(glm::vec3(m_canvas->m_layers.size() + 500)) *
                m_canvas->m_plane_transform[plane_index] *
                glm::translate(glm::vec3(0, 0, -1));

            ShaderManager::use(kShader::Checkerboard);
            ShaderManager::u_int(kShaderUniform::Colorize, false);
            ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp);
            m_face_plane.draw_fill();

            int z = 1;
            auto plane_mvp_z = proj * camera *
                //glm::scale(glm::vec3(z + 1)) *
                //glm::eulerAngleYXZ(yaw, pitch, roll) *
                m_canvas->m_plane_transform[plane_index] *
                glm::translate(glm::vec3(0, 0, -1));

            m_sampler.bind(0);
            ShaderManager::use(kShader::TextureAlpha);
            ShaderManager::u_int(kShaderUniform::Tex, 0);
            ShaderManager::u_float(kShaderUniform::Alpha, 1.f);
            ShaderManager::u_int(kShaderUniform::Highlight, false);
            ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp_z);

            set_active_texture_unit(0);
            m_canvas->m_layers_merge.rtt(plane_index).bindTexture();
            m_face_plane.draw_fill();
            m_canvas->m_layers_merge.rtt(plane_index).unbindTexture();
        }
    }
    else
    {
        const auto blend_gate = node_canvas_blend_gate_plan(
            m_cache_rtt.getWidth(),
            m_cache_rtt.getHeight(),
            m_canvas->m_layers,
            m_canvas->m_current_stroke ? m_canvas->m_current_stroke->m_brush.get() : nullptr);
        const bool use_blend = blend_gate.shader_blend;
        const bool copy_blend_destination = use_blend && !blend_gate.reads_destination_color;

        if (use_blend)
        {
            apply_node_canvas_viewport(0, 0, m_cache_rtt.getWidth(), m_cache_rtt.getHeight());
            m_cache_rtt.bindFramebuffer();
            m_cache_rtt.clear({ 1, 1, 1, 0 });
        }
        else
        {
            // draw the grid
            for (int plane_index = 0; plane_index < 6; plane_index++)
            {
                auto plane_mvp = proj * camera *
                    glm::scale(glm::vec3(m_canvas->m_layers.size() + 500)) *
                    m_canvas->m_plane_transform[plane_index] *
                    glm::translate(glm::vec3(0, 0, -1));

                ShaderManager::use(kShader::Checkerboard);
                ShaderManager::u_int(kShaderUniform::Colorize, false);
                ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp);
                m_face_plane.draw_fill();
            }
        }

        // if not using shader blend, use gl rasterizer blend
        use_blend ? apply_node_canvas_capability(pp::renderer::gl::blend_state(), false) : apply_node_canvas_capability(pp::renderer::gl::blend_state(), true);
        apply_node_canvas_capability(pp::renderer::gl::depth_test_state(), false);

        const auto& b = m_canvas->m_current_stroke->m_brush;

        for (size_t i = 0; i < m_canvas->m_layers.size(); i++)
        {
            auto layer_index = i;
            for (int plane_index = 0; plane_index < 6; plane_index++)
            {
                int onion_size = App::I->animation->get_onion_size();
                int frame_current = m_canvas->m_layers[layer_index]->m_frame_index;
                int frame_start = glm::max<int>(frame_current - onion_size, 0);
                int frame_end = glm::min<int>(frame_current + onion_size, m_canvas->m_layers[layer_index]->frames_count() - 1);
                bool faces = false;
                for (int frame = frame_start; frame <= frame_end; frame++)
                    faces |= m_canvas->m_layers[layer_index]->face(plane_index, frame);
                if (!(m_canvas->m_show_tmp && m_canvas->m_current_layer_idx == layer_index) &&
                    (!m_canvas->m_layers[layer_index]->m_visible ||
                        m_canvas->m_layers[layer_index]->m_opacity == .0f || !faces))
                    continue;

                if (use_blend)
                {
                    m_blender_rtt.bindFramebuffer();
                    m_blender_rtt.clear();
                }

                int z = (int)(m_canvas->m_layers.size() - i);
                auto plane_mvp_z = proj * camera * 
                    glm::scale(glm::vec3(z + 1)) * 
                    glm::eulerAngleYXZ(yaw, pitch, roll) *
                    m_canvas->m_plane_transform[plane_index] * 
                    glm::translate(glm::vec3(0, 0, -1));

                if (m_canvas->m_current_stroke && m_canvas->m_current_mode == kCanvasMode::Erase && m_canvas->m_show_tmp && m_canvas->m_current_layer_idx == layer_index)
                {
                    m_sampler.bind(0);
                    m_sampler.bind(1);
                    m_sampler.bind(2);

                    ShaderManager::use(kShader::CompErase);
                    ShaderManager::u_int(kShaderUniform::Tex, 0);
                    ShaderManager::u_int(kShaderUniform::TexStroke, 1);
                    ShaderManager::u_int(kShaderUniform::TexMask, 2);
                    //ShaderManager::u_vec2(kShaderUniform::Resolution, zw(m_canvas->m_box) / zoom);
                    //ShaderManager::u_int(kShaderUniform::Lock, m_canvas->m_layers[layer_index]->m_alpha_locked);
                    ShaderManager::u_int(kShaderUniform::Mask, m_canvas->m_smask_active);
                    ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp_z);
                    set_active_texture_unit(1);
                    m_canvas->m_tmp[plane_index].bindTexture();
                    set_active_texture_unit(2);
                    m_canvas->m_smask.rtt(plane_index).bindTexture();
                    for (int frame = frame_start; frame <= frame_end; frame++)
                    {
                        float onion_alpha = 1.f - (float)glm::abs(frame - frame_current) / (float)(onion_size + 1);
                        ShaderManager::u_float(kShaderUniform::Alpha, m_canvas->m_layers[layer_index]->m_opacity* onion_alpha);
                        set_active_texture_unit(0);
                        m_canvas->m_layers[layer_index]->rtt(plane_index, frame).bindTexture();
                        m_face_plane.draw_fill();
                        set_active_texture_unit(0);
                        m_canvas->m_layers[layer_index]->rtt(plane_index, frame).unbindTexture();
                    }
                    set_active_texture_unit(2);
                    m_canvas->m_smask.rtt(plane_index).unbindTexture();
                    set_active_texture_unit(1);
                    m_canvas->m_tmp[plane_index].unbindTexture();
                }
                else if(m_canvas->m_current_stroke && m_canvas->m_show_tmp && m_canvas->m_current_layer_idx == layer_index)
                {
                    m_sampler.bind(0);
                    m_sampler.bind(1);
                    m_sampler.bind(2);
                    m_sampler.bind(3);
                    m_sampler_stencil.bind(4);

                    glm::vec2 patt_scale = glm::vec2(b->m_pattern_scale);
                    if (b->m_pattern_flipx) patt_scale.x *= -1.f;
                    if (b->m_pattern_flipy) patt_scale.y *= -1.f;
                
                    ShaderManager::use(kShader::CompDraw);
                    ShaderManager::u_int(kShaderUniform::Tex, 0);
                    ShaderManager::u_int(kShaderUniform::TexStroke, 1);
                    ShaderManager::u_int(kShaderUniform::TexMask, 2);
                    ShaderManager::u_int(kShaderUniform::TexDual, 3);
                    ShaderManager::u_int(kShaderUniform::TexPattern, 4);
                    ShaderManager::u_vec2(kShaderUniform::Resolution, Canvas::I->m_size);
                    ShaderManager::u_int(kShaderUniform::Mask, m_canvas->m_smask_active);
                    ShaderManager::u_int(kShaderUniform::Lock, m_canvas->m_layers[layer_index]->m_alpha_locked);
                    ShaderManager::u_int(kShaderUniform::UseFragcoord, false);
                    ShaderManager::u_int(kShaderUniform::BlendMode, b->m_blend_mode);
                    ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp_z);
                    ShaderManager::u_int(kShaderUniform::UseDual, b->m_dual_enabled);
                    ShaderManager::u_int(kShaderUniform::DualBlendMode, b->m_dual_blend_mode);
                    ShaderManager::u_float(kShaderUniform::DualAlpha, b->m_dual_opacity);
                    ShaderManager::u_int(kShaderUniform::UsePattern, b->m_pattern_enabled && !b->m_pattern_eachsample);
                    ShaderManager::u_vec2(kShaderUniform::PatternScale, patt_scale);
                    ShaderManager::u_float(kShaderUniform::PatternInvert, b->m_pattern_invert);
                    ShaderManager::u_float(kShaderUniform::PatternBright, b->m_pattern_brightness);
                    ShaderManager::u_float(kShaderUniform::PatternContrast, b->m_pattern_contrast);
                    ShaderManager::u_float(kShaderUniform::PatternDepth, b->m_pattern_depth);
                    ShaderManager::u_int(kShaderUniform::PatternBlendMode, b->m_pattern_blend_mode);
                    ShaderManager::u_vec2(kShaderUniform::PatternOffset, Canvas::I->m_pattern_offset);

                    set_active_texture_unit(1);
                    m_canvas->m_tmp[plane_index].bindTexture();
                    set_active_texture_unit(2);
                    m_canvas->m_smask.rtt(plane_index).bindTexture();
                    set_active_texture_unit(3);
                    if (b->m_dual_enabled)
                        m_canvas->m_tmp_dual[plane_index].bindTexture();
                    set_active_texture_unit(4);
                    b->m_pattern_texture ?
                        b->m_pattern_texture->bind() :
                        unbind_texture_2d();
                    for (int frame = frame_start; frame <= frame_end; frame++)
                    {
                        float onion_alpha = 1.f - (float)glm::abs(frame - frame_current) / (float)(onion_size + 1);
                        ShaderManager::u_float(kShaderUniform::Alpha, m_canvas->m_layers[layer_index]->m_opacity * onion_alpha);
                        set_active_texture_unit(0);
                        m_canvas->m_layers[layer_index]->rtt(plane_index, frame).bindTexture();
                        m_face_plane.draw_fill();
                        set_active_texture_unit(0);
                        m_canvas->m_layers[layer_index]->rtt(plane_index, frame).unbindTexture();
                    }
                    set_active_texture_unit(3);
                    if (b->m_dual_enabled)
                        m_canvas->m_tmp_dual[plane_index].unbindTexture();
                    set_active_texture_unit(2);
                    m_canvas->m_smask.rtt(plane_index).unbindTexture();
                    set_active_texture_unit(1);
                    m_canvas->m_tmp[plane_index].unbindTexture();
                }
                else
                {
                    m_canvas->m_cam_fov < 20.f ? m_sampler_nearest.bind(0) : m_sampler.bind(0);
                    ShaderManager::use(kShader::TextureAlpha);
                    ShaderManager::u_int(kShaderUniform::Tex, 0);
                    ShaderManager::u_int(kShaderUniform::Highlight, m_canvas->m_layers[layer_index]->m_hightlight);
                    ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp_z);
                
                    for (int frame = frame_start; frame <= frame_end; frame++)
                    {
                        float onion_alpha = 1.f - (float)glm::abs(frame - frame_current) / (float)(onion_size + 1);
                        ShaderManager::u_float(kShaderUniform::Alpha, m_canvas->m_layers[layer_index]->m_opacity * onion_alpha);
                        set_active_texture_unit(0);
                        m_canvas->m_layers[layer_index]->rtt(plane_index, frame).bindTexture();
                        m_face_plane.draw_fill();
                        m_canvas->m_layers[layer_index]->rtt(plane_index, frame).unbindTexture();
                    }
                }

                if (use_blend)
                {
                    m_blender_rtt.unbindFramebuffer();
                }

                // draw the blended
                if (use_blend)
                {
                    m_sampler.bind(0);
                    m_sampler.bind(2);

                    ShaderManager::use(kShader::TextureBlend);
                    ShaderManager::u_int(kShaderUniform::Tex, 0);
                    if (copy_blend_destination)
                        ShaderManager::u_int(kShaderUniform::TexBG, 2);
                    ShaderManager::u_int(kShaderUniform::BlendMode, m_canvas->m_layers[layer_index]->m_blend_mode);
                    ShaderManager::u_float(kShaderUniform::Alpha, 1.f);
                    ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-1, 1, -1, 1));
                
                    set_active_texture_unit(0);
                    m_blender_rtt.bindTexture();
                    if (copy_blend_destination)
                    {
                        set_active_texture_unit(2);
                        m_blender_bg.bind();
                        copy_framebuffer_to_texture_2d(
                            0,
                            0,
                            0,
                            0,
                            m_blender_bg.size().x,
                            m_blender_bg.size().y);
                    }

                    m_face_plane.draw_fill();

                    if (copy_blend_destination)
                    {
                        set_active_texture_unit(2);
                        m_blender_bg.unbind();
                    }
                    set_active_texture_unit(0);
                    m_blender_rtt.unbindTexture();
                }

    #ifdef _DEBUG
                // draw dirty area
                {
                    auto bb = m_canvas->m_layers[layer_index]->box(plane_index) / (float)m_canvas->m_layers[layer_index]->w;
                    glm::vec2 bbmin = xy(bb);
                    glm::vec2 bbsz = zw(bb) - xy(bb);
                    ShaderManager::use(kShader::Color);
                    ShaderManager::u_vec4(kShaderUniform::Col, { 1, 0, 0, 1 });
                    ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp_z 
                        * glm::translate(glm::vec3(bbmin * 2.f, 0))
                        * glm::translate(glm::vec3(-1, -1, 0))
                        * glm::scale(glm::vec3(bbsz, 1))
                        * glm::translate(glm::vec3(1, 1, 0))
                    );
                    m_face_plane.draw_stroke();
                }
    #endif
            }
        }
        if (use_blend)
        {
            m_cache_rtt.unbindFramebuffer();
            if (m_density != 1.f)
                apply_node_canvas_viewport(0, 0, m_rtt.getWidth(), m_rtt.getHeight());
            else
                apply_node_canvas_viewport(c.x + App::I->off_x, c.y + App::I->off_y, c.z, c.w);
        }

        // draw the grid behind the layers using a temporary copy
        if (use_blend)
        {
            apply_node_canvas_capability(pp::renderer::gl::blend_state(), true);
        
            //draw the grid
            for (int plane_index = 0; plane_index < 6; plane_index++)
            {
                auto plane_mvp = proj * camera *
                    glm::scale(glm::vec3(m_canvas->m_layers.size() + 500.f)) *
                    m_canvas->m_plane_transform[plane_index] *
                    glm::translate(glm::vec3(0, 0, -1.f));

                ShaderManager::use(kShader::Checkerboard);
                ShaderManager::u_int(kShaderUniform::Colorize, false);
                ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp);
                m_face_plane.draw_fill();
            }

            // draw the layers
            m_sampler.bind(0);
            set_active_texture_unit(0);
            m_cache_rtt.bindTexture();
            ShaderManager::use(kShader::Texture);
            ShaderManager::u_int(kShaderUniform::Tex, 0);
            ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho<float>(-1, 1, -1, 1));
            m_face_plane.draw_fill();
            m_cache_rtt.unbindTexture();
        }
    }

    apply_node_canvas_capability(pp::renderer::gl::depth_test_state(), false);

    if (m_canvas->m_smask_active || m_canvas->m_current_mode == kCanvasMode::Copy || m_canvas->m_current_mode == kCanvasMode::Cut)
    {
        if (m_canvas->m_smask_mode == 1)
            m_canvas->modes[(int)kCanvasMode::MaskFree][0]->on_Draw(ortho_proj, proj, camera);
        else if (m_canvas->m_smask_mode == 2)
            m_canvas->modes[(int)kCanvasMode::MaskLine][0]->on_Draw(ortho_proj, proj, camera);
    }

    if (m_canvas->m_smask_active)
    {
        ShaderManager::use(kShader::TextureMask);
        ShaderManager::u_int(kShaderUniform::Tex, 0);
        ShaderManager::u_vec2(kShaderUniform::PatternOffset, m_outline_pan);
        set_active_texture_unit(0);
        apply_node_canvas_capability(pp::renderer::gl::blend_state(), true);

        //draw the cube faces
        for (int plane_index = 0; plane_index < 6; plane_index++)
        {
            auto plane_mvp = proj * camera *
                glm::scale(glm::vec3(m_canvas->m_layers.size() + 500.f)) *
                m_canvas->m_plane_transform[plane_index] *
                glm::translate(glm::vec3(0, 0, -1.f));

            ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp);
            m_canvas->m_smask.rtt(plane_index).bindTexture();
            m_face_plane.draw_fill();
            m_canvas->m_smask.rtt(plane_index).unbindTexture();
        }


    }
    
    // keep drawing the grids
    if (m_canvas->m_current_mode != kCanvasMode::Grid)
        for (auto& mode : Canvas::modes[(int)kCanvasMode::Grid])
            mode->on_Draw(ortho_proj, proj, camera);

    App::I->grid->draw_heightmap(proj, camera, false);

    for (auto& mode : *m_canvas->m_mode)
        mode->on_Draw(ortho_proj, proj, camera);

    if (m_density != 1.f)
    {
        m_rtt.unbindFramebuffer();

        clear_node_canvas_color_buffer({ 1.f, 1.f, 1.f, 0.f });
        apply_node_canvas_viewport(c.x + App::I->off_x, c.y + App::I->off_y, c.z, c.w);

        // draw the canvas
        m_sampler_nearest.bind(0);
        set_active_texture_unit(0);
        m_rtt.bindTexture();
        ShaderManager::use(kShader::Texture);
        ShaderManager::u_int(kShaderUniform::Tex, 0);
        ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho<float>(-1, 1, -1, 1));
        m_face_plane.draw_fill();
        m_rtt.unbindTexture();
    }

    scissor ? apply_node_canvas_capability(pp::renderer::gl::scissor_test_state(), true) : apply_node_canvas_capability(pp::renderer::gl::scissor_test_state(), false);
    blend ? apply_node_canvas_capability(pp::renderer::gl::blend_state(), true) : apply_node_canvas_capability(pp::renderer::gl::blend_state(), false);
    depth ? apply_node_canvas_capability(pp::renderer::gl::depth_test_state(), true) : apply_node_canvas_capability(pp::renderer::gl::depth_test_state(), false);
    apply_node_canvas_viewport(vp.x, vp.y, vp.width, vp.height);
    apply_node_canvas_clear_color(cc);
}

void NodeCanvas::handle_resize(glm::vec2 old_size, glm::vec2 new_size, float zoom)
{
    if (new_size.x != m_canvas->m_width || new_size.y != m_canvas->m_height)
    {
        new_size = new_size * m_density;
        create_buffers();
    }
}

kEventResult NodeCanvas::handle_event(Event* e)
{
    static bool stylus_eraser = false;
    Node::handle_event(e);
    MouseEvent* me = static_cast<MouseEvent*>(e);
    KeyEvent* ke = static_cast<KeyEvent*>(e);
    GestureEvent* ge = static_cast<GestureEvent*>(e);
    TouchEvent* te = static_cast<TouchEvent*>(e);
    auto loc = (me->m_pos - m_pos) * root()->m_zoom;

    switch (e->m_type)
    {
    case kEventType::MouseMove:
        if (stylus_eraser != me->m_eraser)
        {
            run_canvas_tool_mode(me->m_eraser ?
                pp::app::CanvasToolMode::erase :
                pp::app::CanvasToolMode::draw);
            stylus_eraser = me->m_eraser;
        }
    case kEventType::MouseScroll:
    case kEventType::MouseDownL:
    case kEventType::MouseUpL:
    case kEventType::MouseDownR:
    case kEventType::MouseUpR:
    case kEventType::MouseCancel:
        m_canvas->m_cur_pos = loc;
        update_cursor();
        for (auto& mode : *m_canvas->m_mode)
            mode->on_MouseEvent(me, loc);
        break;
    case kEventType::MouseUnfocus:
        (*m_canvas->m_mode)[0]->m_draw_tip = false;
        App::I->show_cursor();
        break;
    case kEventType::MouseFocus:
        update_cursor();
        break;
    case kEventType::KeyDown:
        run_canvas_hotkey(
            pp::app::CanvasHotkeyEvent::key_down,
            ke->m_key,
            m_mouse_focus);
        for (auto& mode : *m_canvas->m_mode)
            mode->on_KeyEvent(ke);
     break;
    case kEventType::KeyUp:
        update_cursor();
        run_canvas_hotkey(
            pp::app::CanvasHotkeyEvent::key_up,
            ke->m_key,
            m_mouse_focus);
        for (auto& mode : *m_canvas->m_mode)
            mode->on_KeyEvent(ke);
        break;
    case kEventType::GestureStart:
        mouse_capture();
        for (auto& mode : *m_canvas->m_mode)
            mode->on_GestureEvent(ge);
        break;
    case kEventType::GestureMove:
        for (auto& mode : *m_canvas->m_mode)
            mode->on_GestureEvent(ge);
        break;
    case kEventType::GestureEnd:
        mouse_release();
        for (auto& mode : *m_canvas->m_mode)
            mode->on_GestureEvent(ge);
        break;
    case kEventType::TouchTap:
        run_canvas_hotkey(
            pp::app::CanvasHotkeyEvent::touch_tap,
            kKey::Unknown,
            m_mouse_focus,
            te->m_finger_count);
        break;
    default:
        return kEventResult::Available;
        break;
    }
    return kEventResult::Consumed;
}

void NodeCanvas::reset_camera()
{
    m_canvas->m_cam_rot = glm::mat4(1);
    m_canvas->m_cam_pos = {0, 0, 0};
    m_canvas->m_cam_fov = 85;
    m_canvas->m_pan = {0, 0};
}

void NodeCanvas::create_buffers()
{
    auto new_size = GetSize() * m_density;
    LOG("NodeCanvas::create_buffers size: %d x %d density %f", (int)new_size.x, (int)new_size.y, m_density);
    m_canvas->m_mixer.create((int)new_size.x * m_canvas->m_mixer_scale,
        (int)new_size.y * m_canvas->m_mixer_scale, -1, pp::renderer::gl::rgba8_internal_format());
    m_blender_rtt.create((int)new_size.x, (int)new_size.y, -1, pp::renderer::gl::rgba8_internal_format());
    m_cache_rtt.create((int)new_size.x, (int)new_size.y, -1, pp::renderer::gl::rgba8_internal_format());
    m_rtt.create((int)new_size.x, (int)new_size.y, -1, pp::renderer::gl::rgba8_internal_format(), true);
    m_blender_bg.create((int)new_size.x, (int)new_size.y, pp::renderer::gl::rgba8_internal_format());
}

void NodeCanvas::set_density(float d)
{
    m_density = d;
    create_buffers();
}

void NodeCanvas::set_cursor_visibility(kCursorVisibility mode)
{
    m_cursor_visibility = mode;
}

void NodeCanvas::update_cursor()
{
    bool visible = true;
    if (m_canvas->m_current_mode == kCanvasMode::Draw ||
        m_canvas->m_current_mode == kCanvasMode::Erase)
    {
        if (m_cursor_visibility == kCursorVisibility::Always)
            visible = true;
        if (m_cursor_visibility == kCursorVisibility::Never)
            visible = false;
        if (m_cursor_visibility == kCursorVisibility::SmallBrush)
            visible = m_canvas->m_current_brush->m_tip_size < 10;
        if (m_cursor_visibility == kCursorVisibility::NotPainting)
            visible = !m_canvas->get_mode<CanvasModePen>()->m_drawing;
        if (App::I->keys[(int)kKey::KeyAlt] ||
            m_canvas->get_mode<CanvasModePen>()->m_resizing ||
            m_canvas->get_mode<CanvasModePen>()->m_picking)
            visible = true;
    }
    visible ? App::I->show_cursor() : App::I->hide_cursor();
}

void NodeCanvas::on_tick(float dt)
{
    m_outline_pan = glm::fract(m_outline_pan + dt * 0.01f);
}

void NodeCanvas::destroy()
{
    m_blender_rtt.destroy();
    m_cache_rtt.destroy();
    m_rtt.destroy();
    m_blender_bg.destroy();
    m_face_plane.destroy();
    m_line.destroy();
    m_grid.destroy();
    Node::destroy();
}
