#include "pch.h"

#include "legacy_node_canvas_draw_services.h"

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

#include "app.h"
#include "legacy_canvas_draw_merge_services.h"
#include "legacy_preference_storage.h"
#include "legacy_ui_gl_dispatch.h"
#include "legacy_ui_overlay_services.h"
#include "log.h"
#include "node_panel_grid.h"
#include "node_image_texture.h"
#include "paint_renderer/compositor.h"
#include "renderer_gl/opengl_capabilities.h"
#include "util.h"

namespace {

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

void unbind_texture_2d()
{
    pp::legacy::ui_gl::unbind_texture_2d("NodeCanvas");
}

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

pp::renderer::gl::OpenGlViewportRect query_node_canvas_viewport()
{
    return pp::legacy::ui_gl::query_viewport_rect("NodeCanvas");
}

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

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

void clear_node_canvas_color_buffer(std::array<float, 4> color)
{
    pp::legacy::ui_gl::clear_color_buffer(color, "NodeCanvas");
}

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

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

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::panopainter::LegacyCanvasDrawMergeLayerPathExecution make_node_canvas_layer_path_execution(
    NodeCanvas& node_canvas,
    size_t layer_index,
    int plane_index,
    const glm::mat4& plane_mvp_z,
    const Brush* brush,
    bool copy_blend_destination,
    bool use_nearest_sampler)
{
    auto* layer = node_canvas.m_canvas->m_layers[layer_index].get();
    const auto plane_mvp = plane_mvp_z;
    const auto brush_ptr = brush;
    const auto smask_active = node_canvas.m_canvas->m_smask_active;
    auto draw_layer_frame = pp::panopainter::make_legacy_canvas_draw_merge_layer_frame_draw(
        layer,
        &node_canvas.m_face_plane,
        set_active_texture_unit,
        plane_index,
        layer->m_opacity);

    glm::vec2 patt_scale = glm::vec2(brush_ptr->m_pattern_scale);
    if (brush_ptr->m_pattern_flipx)
        patt_scale.x *= -1.f;
    if (brush_ptr->m_pattern_flipy)
        patt_scale.y *= -1.f;

    return pp::panopainter::make_legacy_canvas_draw_merge_layer_path_gl_execution(
        {
            .temporary_erase = {
                .mvp = plane_mvp,
                .texture_slot = 0,
                .stroke_texture_slot = 1,
                .mask_texture_slot = 2,
                .mask_enabled = smask_active,
            },
            .temporary_paint = {
                .resolution = Canvas::I->m_size,
                .pattern = {
                    .scale = patt_scale,
                    .invert = static_cast<float>(brush_ptr->m_pattern_invert),
                    .brightness = brush_ptr->m_pattern_brightness,
                    .contrast = brush_ptr->m_pattern_contrast,
                    .depth = brush_ptr->m_pattern_depth,
                    .blend_mode = brush_ptr->m_pattern_blend_mode,
                    .offset = Canvas::I->m_pattern_offset,
                },
                .mvp = plane_mvp,
                .layer_alpha = 1.0f,
                .alpha_lock = layer->m_alpha_locked,
                .mask_enabled = smask_active,
                .use_fragcoord = false,
                .blend_mode = brush_ptr->m_blend_mode,
                .use_dual = brush_ptr->m_dual_enabled,
                .dual_blend_mode = brush_ptr->m_dual_blend_mode,
                .dual_alpha = brush_ptr->m_dual_opacity,
                .use_pattern = brush_ptr->m_pattern_enabled && !brush_ptr->m_pattern_eachsample,
            },
            .layer_texture = {
                .mvp = plane_mvp,
                .texture_slot = 0,
                .alpha = 1.f,
                .highlight = layer->m_hightlight,
            },
            .blend = {
                .shader = {
                    .mvp = glm::ortho(-1, 1, -1, 1),
                    .texture_slot = 0,
                    .destination_texture_slot = 2,
                    .use_destination_texture = copy_blend_destination,
                    .blend_mode = layer->m_blend_mode,
                    .alpha = 1.f,
                },
                .copy_destination = copy_blend_destination,
            },
            .use_nearest_sampler = use_nearest_sampler,
            .use_dual_texture = brush_ptr->m_dual_enabled,
        },
        [&node_canvas] {
            node_canvas.m_blender_rtt.bindFramebuffer();
        },
        [&node_canvas] {
            node_canvas.m_blender_rtt.clear();
        },
        [&node_canvas] {
            node_canvas.m_blender_rtt.unbindFramebuffer();
        },
        [&node_canvas](int unit) {
            node_canvas.m_sampler.bind(unit);
        },
        [&node_canvas](int unit) {
            node_canvas.m_sampler_nearest.bind(unit);
        },
        [&node_canvas](int unit) {
            node_canvas.m_sampler_stencil.bind(unit);
        },
        [](int unit) {
            set_active_texture_unit(unit);
        },
        [&node_canvas, plane_index] {
            node_canvas.m_canvas->m_tmp[plane_index].bindTexture();
        },
        [&node_canvas, plane_index] {
            node_canvas.m_canvas->m_tmp[plane_index].unbindTexture();
        },
        [&node_canvas, plane_index] {
            node_canvas.m_canvas->m_smask.rtt(plane_index).bindTexture();
        },
        [&node_canvas, plane_index] {
            node_canvas.m_canvas->m_smask.rtt(plane_index).unbindTexture();
        },
        [&node_canvas, plane_index] {
            node_canvas.m_canvas->m_tmp_dual[plane_index].bindTexture();
        },
        [&node_canvas, plane_index] {
            node_canvas.m_canvas->m_tmp_dual[plane_index].unbindTexture();
        },
        [brush_ptr] {
            brush_ptr->m_pattern_texture ? brush_ptr->m_pattern_texture->bind() : unbind_texture_2d();
        },
        [&node_canvas] {
            node_canvas.m_face_plane.draw_fill();
        },
        [&node_canvas] {
            node_canvas.m_blender_rtt.bindTexture();
        },
        [&node_canvas] {
            node_canvas.m_blender_rtt.unbindTexture();
        },
        [&node_canvas] {
            node_canvas.m_blender_bg.bind();
        },
        [&node_canvas] {
            node_canvas.m_blender_bg.unbind();
        },
        [&node_canvas] {
            copy_framebuffer_to_texture_2d(
                0,
                0,
                0,
                0,
                node_canvas.m_blender_bg.size().x,
                node_canvas.m_blender_bg.size().y);
        },
#ifdef _DEBUG
        [&node_canvas, layer_index, plane_index, plane_mvp] {
            auto bb = node_canvas.m_canvas->m_layers[layer_index]->box(plane_index) / (float)node_canvas.m_canvas->m_layers[layer_index]->w;
            glm::vec2 bbmin = xy(bb);
            glm::vec2 bbsz = zw(bb) - xy(bb);
            pp::panopainter::configure_legacy_ui_color_shader(
                plane_mvp
                    * 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)),
                { 1, 0, 0, 1 });
            node_canvas.m_face_plane.draw_stroke();
        },
#else
        [] {
        },
#endif
        draw_layer_frame);
}

void execute_node_canvas_draw_unmerged_pass(
    NodeCanvas& node_canvas,
    const glm::mat4& proj,
    const glm::mat4& camera,
    const glm::ivec4& c,
    float yaw,
    float pitch,
    float roll)
{
    const auto blend_gate = node_canvas_blend_gate_plan(
        node_canvas.m_cache_rtt.getWidth(),
        node_canvas.m_cache_rtt.getHeight(),
        node_canvas.m_canvas->m_layers,
        node_canvas.m_canvas->m_current_stroke ? node_canvas.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;
    const auto layer_orientation = glm::eulerAngleYXZ(yaw, pitch, roll);

    pp::panopainter::execute_legacy_canvas_draw_node_canvas_unmerged_pass(
        node_canvas,
        use_blend,
        copy_blend_destination,
        proj,
        camera,
        layer_orientation,
        c,
        [&](int x, int y, int width, int height) {
            apply_node_canvas_viewport(x, y, width, height);
        },
        [&](auto state, bool enabled) {
            apply_node_canvas_capability(state, enabled);
        },
        [&] {
            node_canvas.m_sampler.bind(0);
            set_active_texture_unit(0);
        },
        [&](size_t layer_index, int plane_index, const glm::mat4& plane_mvp_z, const Brush* brush, bool copy_blend_destination, bool use_nearest_sampler) {
            return make_node_canvas_layer_path_execution(
                node_canvas,
                layer_index,
                plane_index,
                plane_mvp_z,
                brush,
                copy_blend_destination,
                use_nearest_sampler);
        },
        [&](const char* message) {
            LOG("NodeCanvas onion frame range failed: %s", message);
            });
}

void execute_node_canvas_draw_merged_pass(
    NodeCanvas& node_canvas,
    const glm::mat4& proj,
    const glm::mat4& camera)
{
    pp::panopainter::execute_legacy_canvas_draw_merged_pass(
        node_canvas,
        proj,
        camera,
        [&](auto state, bool enabled) {
            apply_node_canvas_capability(state, enabled);
        },
        [](int unit) {
            set_active_texture_unit(unit);
        },
        [&] {
            node_canvas.m_face_plane.draw_fill();
        });
}

void execute_node_canvas_draw_merge_tail(
    NodeCanvas& node_canvas,
    const glm::mat4& ortho_proj,
    const glm::mat4& proj,
    const glm::mat4& camera,
    const glm::ivec4& c)
{
    apply_node_canvas_capability(pp::renderer::gl::depth_test_state(), false);

    pp::panopainter::execute_legacy_canvas_draw_merge_post_draw_callbacks(
        node_canvas.m_canvas->m_smask_active,
        node_canvas.m_canvas->m_current_mode == kCanvasMode::Copy || node_canvas.m_canvas->m_current_mode == kCanvasMode::Cut,
        node_canvas.m_canvas->m_smask_mode,
        node_canvas.m_canvas->m_current_mode != kCanvasMode::Grid,
        [&] {
            node_canvas.m_canvas->modes[(int)kCanvasMode::MaskFree][0]->on_Draw(ortho_proj, proj, camera);
        },
        [&] {
            node_canvas.m_canvas->modes[(int)kCanvasMode::MaskLine][0]->on_Draw(ortho_proj, proj, camera);
        },
        [&] {
            pp::panopainter::execute_legacy_canvas_draw_merge_smask_faces(
                pp::panopainter::LegacyCanvasDrawMergeTextureMaskUniforms {
                    .texture_slot = 0,
                    .pattern_offset = node_canvas.m_outline_pan,
                },
                proj,
                camera,
                node_canvas.m_canvas->m_layers.size() + 500.f,
                std::to_array(node_canvas.m_canvas->m_plane_transform),
                {
                    .set_active_texture_unit = [&] {
                        set_active_texture_unit(0);
                    },
                    .enable_blend = [&] {
                        apply_node_canvas_capability(pp::renderer::gl::blend_state(), true);
                    },
                    .bind_face_texture = [&](int plane_index) {
                        node_canvas.m_canvas->m_smask.rtt(plane_index).bindTexture();
                    },
                    .draw_face = [&] {
                        node_canvas.m_face_plane.draw_fill();
                    },
                    .unbind_face_texture = [&](int plane_index) {
                        node_canvas.m_canvas->m_smask.rtt(plane_index).unbindTexture();
                    },
                });
        },
        pp::panopainter::make_legacy_canvas_draw_merge_grid_modes_draw(
            &Canvas::modes[(int)kCanvasMode::Grid],
            ortho_proj,
            proj,
            camera),
        pp::panopainter::make_legacy_canvas_draw_merge_heightmap_draw(App::I->grid.get(), proj, camera),
        pp::panopainter::make_legacy_canvas_draw_merge_current_modes_draw(
            node_canvas.m_canvas->m_mode,
            ortho_proj,
            proj,
            camera));

    if (node_canvas.m_density != 1.f) {
        pp::panopainter::execute_legacy_canvas_draw_merge_display_resolve(
            pp::panopainter::LegacyCanvasDrawMergeDisplayResolveUniforms {
                .texture = {
                    .mvp = glm::ortho<float>(-1, 1, -1, 1),
                    .texture_slot = 0,
                },
            },
            {
                .unbind_resolve_framebuffer = [&] {
                    node_canvas.m_rtt.unbindFramebuffer();
                },
                .clear_color_buffer = [&] {
                    clear_node_canvas_color_buffer({ 1.f, 1.f, 1.f, 0.f });
                },
                .apply_viewport = [&] {
                    apply_node_canvas_viewport(c.x + App::I->off_x, c.y + App::I->off_y, c.z, c.w);
                },
                .bind_sampler = [&] {
                    node_canvas.m_sampler_nearest.bind(0);
                },
                .bind_resolve_texture = [&] {
                    set_active_texture_unit(0);
                    node_canvas.m_rtt.bindTexture();
                },
                .draw = [&] {
                    node_canvas.m_face_plane.draw_fill();
                },
                .unbind_resolve_texture = [&] {
                    node_canvas.m_rtt.unbindTexture();
                },
            });
    }
}

} // namespace

namespace pp::panopainter {

void init_legacy_node_canvas_shell(NodeCanvas& node_canvas)
{
    const auto preferences = read_legacy_canvas_preferences();
    node_canvas.m_density = preferences.viewport_density;
    node_canvas.m_cursor_visibility = (NodeCanvas::kCursorVisibility)preferences.cursor_mode;

    node_canvas.m_mouse_ignore = false;
    node_canvas.m_canvas = std::make_unique<Canvas>();
    const int canvas_resolution = App::I->default_canvas_resolution();
    node_canvas.m_canvas->create(canvas_resolution, canvas_resolution);
    node_canvas.m_canvas->m_unsaved = false;
    node_canvas.m_canvas->m_node = &node_canvas;

    node_canvas.m_sampler.create();
    //node_canvas.m_sampler.set_filter(pp::renderer::gl::linear_texture_filter(), pp::renderer::gl::nearest_texture_filter());

    node_canvas.m_sampler_nearest.create(pp::renderer::gl::nearest_texture_filter());

    node_canvas.m_sampler_linear.create(pp::renderer::gl::linear_texture_filter());
    node_canvas.m_sampler_stencil.create(
        pp::renderer::gl::linear_texture_filter(),
        pp::renderer::gl::repeat_texture_wrap());
    node_canvas.m_face_plane.create<1>(2, 2);
    node_canvas.m_line.create();
    CanvasMode::node = &node_canvas;
    for (int i = 0; i < (int)kCanvasMode::COUNT; i++)
        for (auto m : Canvas::modes[i])
            m->init();

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

void draw_legacy_node_canvas_shell(NodeCanvas& node_canvas)
{
    // sanity checks
    float zoom = node_canvas.root()->m_zoom;
    if (zoom == 0.f)
        zoom = 1.f;
    auto box = node_canvas.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(node_canvas.m_canvas->m_cam_fov), box.z / box.w, 0.001f, 1000.f);
    glm::mat4 camera = glm::translate(node_canvas.m_canvas->m_cam_pos) * node_canvas.m_canvas->m_cam_rot;

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

    prepare_legacy_node_canvas_draw_setup(node_canvas, box, c, proj, camera);

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

    execute_legacy_canvas_draw_node_canvas_shell(
        node_canvas.m_density != 1.f,
        draw_merged,
        [&] {
            node_canvas.m_rtt.bindFramebuffer();
            clear_node_canvas_color_buffer({ 1.f, 1.f, 0.f, 0.f });
            apply_node_canvas_viewport(0, 0, node_canvas.m_rtt.getWidth(), node_canvas.m_rtt.getHeight());
        },
        [&] {
            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);
        },
        [&] {
            execute_node_canvas_draw_merged_pass(node_canvas, proj, camera);
        },
        [&] {
            execute_node_canvas_draw_unmerged_pass(node_canvas, proj, camera, c, yaw, pitch, roll);
        });

    execute_node_canvas_draw_merge_tail(node_canvas, ortho_proj, proj, camera, c);

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

} // namespace pp::panopainter
