#include "pch.h"

#include "canvas.h"
#include "app.h"
#include "legacy_canvas_stroke_runtime_services.h"

#include "legacy_canvas_stroke_composite_services.h"
#include "legacy_canvas_stroke_services.h"
#include "legacy_ui_gl_dispatch.h"
#include "renderer_gl/opengl_capabilities.h"

#include <algorithm>
#include <array>
#include <cstdint>

namespace {

GLenum depth_test_state()
{
    return static_cast<GLenum>(pp::renderer::gl::depth_test_state());
}

GLenum scissor_test_state()
{
    return static_cast<GLenum>(pp::renderer::gl::scissor_test_state());
}

GLenum blend_state()
{
    return static_cast<GLenum>(pp::renderer::gl::blend_state());
}

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

void apply_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, "Canvas");
}

void apply_canvas_scissor(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height)
{
    pp::legacy::ui_gl::apply_scissor_rect(x, y, width, height, "Canvas");
}

void apply_canvas_capability(std::uint32_t capability, bool enabled)
{
    pp::legacy::ui_gl::set_capability(capability, enabled, "Canvas");
}

} // namespace

namespace pp::panopainter {

LegacyCanvasStrokeMixPassShell make_legacy_canvas_stroke_mix_pass_shell(
    Canvas& canvas,
    const glm::vec2& bb_min,
    const glm::vec2& bb_sz)
{
    const auto layer_index = canvas.m_current_layer_idx;
    auto& current_layer = *canvas.m_layers[layer_index];
    std::array<glm::mat4, 6> plane_transform {};
    std::copy(std::begin(Canvas::m_plane_transform), std::end(Canvas::m_plane_transform), plane_transform.begin());
    const auto mix_planes = pp::panopainter::plan_legacy_canvas_stroke_mix_pass_planes(
        current_layer.m_visible,
        current_layer.m_opacity,
        glm::scale(glm::vec3(1, -1, 1)) * canvas.m_proj * canvas.m_mv,
        plane_transform,
        [&](int plane_index) {
            return current_layer.face(plane_index);
        });
    const auto& b = canvas.m_current_stroke->m_brush;
    return pp::panopainter::make_legacy_canvas_stroke_mix_pass_shell(
        [&] {
            canvas.m_mixer.bindFramebuffer();
            apply_canvas_viewport(0, 0, canvas.m_mixer.getWidth(), canvas.m_mixer.getHeight());
            apply_canvas_capability(depth_test_state(), false);
            apply_canvas_capability(scissor_test_state(), true);
            apply_canvas_capability(blend_state(), false);
            apply_canvas_scissor(
                static_cast<std::int32_t>(bb_min.x),
                static_cast<std::int32_t>(bb_min.y),
                static_cast<std::int32_t>(bb_sz.x),
                static_cast<std::int32_t>(bb_sz.y));
        },
        [&] {
            canvas.m_mixer.unbindFramebuffer();
        },
        "Canvas::stroke_draw_mix",
        canvas.m_size,
        mix_planes,
        [&] {
            canvas.m_sampler.bind(0);
            canvas.m_sampler.bind(1);
            canvas.m_sampler.bind(2);
        },
        [&] {
            canvas.m_sampler.unbind();
        },
        [&](int plane_index, const glm::mat4& plane_mvp_z) {
            (void)plane_index;
            pp::panopainter::setup_legacy_stroke_composite_shader(
                pp::panopainter::LegacyStrokeCompositeUniforms {
                    .resolution = canvas.m_size,
                    .mvp = plane_mvp_z,
                    .pattern_texture_slot = 3,
                    .layer_alpha = 1.0f,
                    .alpha_lock = false,
                    .mask_enabled = false,
                    .use_fragcoord = false,
                    .blend_mode = b->m_blend_mode,
                    .use_dual = false,
                    .use_pattern = false,
                });
        },
        [&](int plane_index) {
            set_active_texture_unit(0);
            current_layer.rtt(plane_index).bindTexture();
        },
        [&](int plane_index) {
            set_active_texture_unit(1);
            canvas.m_tmp[plane_index].bindTexture();
        },
        [&](int plane_index) {
            set_active_texture_unit(2);
            canvas.m_smask.rtt(plane_index).bindTexture();
        },
        [&] {
            canvas.m_node->m_face_plane.draw_fill();
        },
        [&](int plane_index) {
            set_active_texture_unit(2);
            canvas.m_smask.rtt(plane_index).unbindTexture();
        },
        [&](int plane_index) {
            set_active_texture_unit(1);
            canvas.m_tmp[plane_index].unbindTexture();
        },
        [&](int plane_index) {
            set_active_texture_unit(0);
            current_layer.rtt(plane_index).unbindTexture();
        });
}

void legacy_canvas_stroke_end(Canvas& canvas)
{
    if (!canvas.m_current_stroke)
        return;
    if (canvas.m_current_stroke->has_sample())
    {
        canvas.m_commit_delayed = true;
    }
    else
    {
        canvas.m_show_tmp = false;
        canvas.stroke_commit();
        canvas.m_current_stroke = nullptr;
    }
}

void legacy_canvas_stroke_cancel(Canvas& canvas)
{
    if (!canvas.m_current_stroke)
        return;
    canvas.m_current_stroke = nullptr;
    canvas.m_show_tmp = false;
}

void legacy_canvas_stroke_draw_mix(Canvas& canvas, const glm::vec2& bb_min, const glm::vec2& bb_sz)
{
    gl_state gl;
    gl.save();
    const auto mix_shell = make_legacy_canvas_stroke_mix_pass_shell(canvas, bb_min, bb_sz);
    [[maybe_unused]] const auto mix_result = pp::panopainter::execute_legacy_canvas_stroke_mix_pass_shell(
        mix_shell.setup.begin,
        mix_shell.setup.end,
        mix_shell.request);

    gl.restore();
}

std::array<std::vector<vertex_t>, 6> legacy_canvas_stroke_draw_project(
    const Canvas& canvas,
    std::array<vertex_t, 4>& B,
    bool project_3d /*= false*/,
    glm::mat4 mv /*= glm::mat4(1)*/)
{
    const auto unp_vp = zw(canvas.m_box);
    const auto unp_inv = glm::inverse(canvas.m_proj * canvas.m_mv);
    std::array<std::vector<vertex_t>, 6> ret;
    for (int i = 0; i < 6; i++)
    {
        struct ray_t {
            glm::vec3 o;
            glm::vec3 d;
            vertex_t v;
            ray_t(glm::vec3 o, glm::vec3 d, vertex_t v) : o(o), d(d), v(v) { }
        };
        std::vector<ray_t> rays;
        if (project_3d)
        {
            rays.reserve(B.size());
            for (auto const& b : B)
                rays.emplace_back(glm::vec3(0), b.pos, b);
        }
        else
        {
            auto P = poly_intersect(B.data(), B.data() + 4, canvas.m_plane_shape[i]);
            rays.reserve(P.size());
            for (auto const& p : P)
            {
                glm::vec3 ray_origin, ray_dir;
                auto clip_space = glm::vec2(p.pos.x, unp_vp.y - p.pos.y - 1.f) / unp_vp * 2.f - 1.f;
                auto wp0 = unp_inv * glm::vec4(clip_space, 0, 1);
                auto wp1 = unp_inv * glm::vec4(clip_space, .5, 1);
                ray_origin = xyz(wp0 / wp0.w);
                ray_dir = glm::normalize(xyz(wp1 / wp1.w) - ray_origin);
                rays.emplace_back(ray_origin, ray_dir, p);
            }
        }
        glm::mat4 plane_camera = glm::lookAt(canvas.m_plane_origin[i], canvas.m_plane_normal[i], canvas.m_plane_tangent[i]);
        std::vector<vertex_t> face_ret;
        face_ret.reserve(rays.size());
        for (auto const& r : rays)
        {
            glm::vec3 hit;
            float hit_t;
            if (ray_intersect(r.o, r.d, canvas.m_plane_origin[i], canvas.m_plane_normal[i], canvas.m_plane_tangent[i], hit, hit_t))
            {
                glm::vec4 plane_local = plane_camera * glm::vec4(hit, 1);

                vertex_t v;
                v.pos.x = -(plane_local.x * 0.5f - 0.5f) * canvas.m_width;
                v.pos.y = (plane_local.y * 0.5f + 0.5f) * canvas.m_height;

                auto hit_cam = mv * glm::vec4(hit, 1);
                v.pos.z = 0;
                v.pos.w = hit_cam.z;
                v.uvs = r.v.uvs * hit_cam.z;
                v.uvs2 = r.v.uvs2 * hit_cam.z;
                face_ret.emplace_back(v);
            }
            else
            {
                break;
            }
        }
        if (face_ret.size() >= 3)
            ret[i] = std::move(face_ret);
    }
    return ret;
}

void legacy_canvas_stroke_update(Canvas& canvas, glm::vec3 point, float pressure)
{
    canvas.m_current_stroke->add_point(point, pressure);
    if (canvas.m_dual_stroke)
        canvas.m_dual_stroke->add_point(point, pressure);
}

void legacy_canvas_stroke_start(Canvas& canvas, glm::vec3 point, float pressure)
{
    assert(App::I->is_render_thread());

    if (canvas.m_current_stroke && canvas.m_commit_delayed)
    {
        canvas.m_show_tmp = false;
        canvas.m_commit_delayed = false;
        canvas.stroke_commit();
        canvas.m_current_stroke = nullptr;
        canvas.m_dual_stroke = nullptr;
    }

    canvas.m_pattern_offset = canvas.m_current_brush->m_pattern_rand_offset ?
        glm::vec2((rand() % 1000) * 0.001f, (rand() % 1000) * 0.001f) :
        glm::vec2(0);

    canvas.m_current_stroke = std::make_unique<Stroke>();
    canvas.m_current_stroke->m_camera.rot = canvas.m_cam_rot;
    canvas.m_current_stroke->m_camera.fov = canvas.m_cam_fov;
    if (canvas.m_current_mode == kCanvasMode::Line)
        canvas.m_current_stroke->m_filter_points = false;
    canvas.m_current_stroke->randomize_prng();
    canvas.m_current_stroke->start(canvas.m_current_brush);
    canvas.m_current_stroke->add_point(point, pressure);

    if (canvas.m_current_brush->m_dual_enabled)
    {
        auto dual_brush = std::make_shared<Brush>();
        dual_brush->m_tip_flow = canvas.m_current_brush->m_dual_flow;
        dual_brush->m_tip_opacity = canvas.m_current_brush->m_dual_opacity;
        dual_brush->m_tip_flipx = canvas.m_current_brush->m_dual_flipx;
        dual_brush->m_tip_flipy = canvas.m_current_brush->m_dual_flipy;
        dual_brush->m_tip_invert = canvas.m_current_brush->m_dual_invert;
        dual_brush->m_blend_mode = canvas.m_current_brush->m_dual_blend_mode;
        dual_brush->m_tip_randflipx = canvas.m_current_brush->m_dual_randflip;
        dual_brush->m_tip_randflipy = canvas.m_current_brush->m_dual_randflip;
        dual_brush->m_tip_size = canvas.m_current_brush->m_dual_size * canvas.m_current_brush->m_tip_size;
        dual_brush->m_tip_spacing = canvas.m_current_brush->m_dual_spacing;
        dual_brush->m_jitter_scatter = canvas.m_current_brush->m_dual_scatter;
        dual_brush->m_jitter_scatter_bothaxis = canvas.m_current_brush->m_dual_scatter_bothaxis;
        dual_brush->m_jitter_angle = canvas.m_current_brush->m_dual_rotate;
        dual_brush->m_tip_texture = canvas.m_current_brush->m_dual_texture;
        dual_brush->m_tip_aspect = canvas.m_current_brush->m_dual_aspect;
        canvas.m_dual_stroke = std::make_unique<Stroke>();
        canvas.m_dual_stroke->m_camera.rot = canvas.m_cam_rot;
        canvas.m_dual_stroke->m_camera.fov = canvas.m_cam_fov;
        canvas.m_dual_stroke->start(dual_brush);
        canvas.m_dual_stroke->add_point(point, pressure);
    }

    for (int i = 0; i < 6; i++)
    {
        canvas.m_dirty_box[i] = glm::vec4(canvas.m_width, canvas.m_height, 0, 0);
        canvas.m_dirty_face[i] = false;

        canvas.m_tmp[i].bindFramebuffer();
        canvas.m_tmp[i].clear({ 0, 0, 0, 0 });
        canvas.m_tmp[i].unbindFramebuffer();

        if (canvas.m_current_brush->m_dual_enabled)
        {
            canvas.m_tmp_dual[i].bindFramebuffer();
            canvas.m_tmp_dual[i].clear({ 0, 0, 0, 0 });
            canvas.m_tmp_dual[i].unbindFramebuffer();
        }
    }
    canvas.m_mixer.bindFramebuffer();
    canvas.m_mixer.clear();
    canvas.m_mixer.unbindFramebuffer();
    canvas.m_show_tmp = true;
}

} // namespace pp::panopainter

void Canvas::stroke_end()
{
    pp::panopainter::legacy_canvas_stroke_end(*this);
}

void Canvas::stroke_cancel()
{
    pp::panopainter::legacy_canvas_stroke_cancel(*this);
}

void Canvas::stroke_draw_mix(const glm::vec2& bb_min, const glm::vec2& bb_sz)
{
    pp::panopainter::legacy_canvas_stroke_draw_mix(*this, bb_min, bb_sz);
}

std::array<std::vector<vertex_t>, 6> Canvas::stroke_draw_project(
    std::array<vertex_t, 4>& B,
    bool project_3d /*= false*/,
    glm::mat4 mv /*= glm::mat4(1)*/) const
{
    return pp::panopainter::legacy_canvas_stroke_draw_project(*this, B, project_3d, mv);
}

void Canvas::stroke_update(glm::vec3 point, float pressure)
{
    pp::panopainter::legacy_canvas_stroke_update(*this, point, pressure);
}

void Canvas::stroke_start(glm::vec3 point, float pressure)
{
    pp::panopainter::legacy_canvas_stroke_start(*this, point, pressure);
}
