#pragma once
#include "rtt.h"
#include "texture.h"
#include "shader.h"
#include "shape.h"
#include "brush.h"
#include "canvas_layer.h"
#include "canvas_actions.h"
#include "canvas_modes.h"
#include "legacy_canvas_stroke_execution_services.h"
#include <stack>
#include "mp4enc.h"

struct PPIThumb
{
    int width = 128;
    int height = 128;
    int comp = 4;
    bool valid() const
    {
        return (width == 128 && height == 128 && comp == 4);
    }
};

struct PPIDocVersion
{
    int major = 0;

    // version 1: initial
    // version 2: added blend mode, alpha and visibility
    // version 3: added animation frames
    // version 4: (released in 0.2.3) add info struct
    int minor = 4;
};

struct PPISoftVersion
{
    int major = g_version_major;
    int minor = g_version_minor;
    int fix = g_version_fix;
    int build = g_version_build;
};

struct PPIHeader
{
    char magic[4]{ 'P', 'P', 'I', 0 };
    PPIDocVersion doc_version;
    PPISoftVersion soft_version;
    PPIThumb thumb_header;
    bool valid()
    {
        if (strcmp(magic, "PPI") != 0)
            return false;
        if (doc_version.major != 0 || (doc_version.minor < 1 && doc_version.minor > 2))
            return false;
        if (!thumb_header.valid())
            return false;
        return true;
    }
};

class Canvas
{
    struct StrokeFrame
    {
        glm::vec4 col;
        float flow;
        float opacity;
        std::array<std::vector<vertex_t>, 6> shapes;
        glm::vec4 m_mixer_rect;
    };

public:
    struct FloodData
    {
        std::shared_ptr<Layer> layer;
        std::unique_ptr<bool[]> mask[6];
        std::unique_ptr<glm::u8vec4[]> rgb[6] = SIXPLETTE(0);
        std::array<bool, 6> dirty = SIXPLETTE(false);
        glm::vec4 bb[6];
        void apply();
    };

    Plane m_plane;
    Plane m_plane_brush;
    DynamicShape m_brush_shape;
    bool m_unsaved = false;
    bool m_newdoc = true;
    bool m_dirty = false;
    bool m_commit_delayed = false;
    bool m_dirty_stroke = false;
    std::unique_ptr<MP4Encoder> m_encoder;
    std::chrono::time_point<std::chrono::steady_clock> m_disrty_stroke_time;
    std::stack<CameraData> m_camera_stack;

    static Canvas* I;
    NodeCanvas* m_node = nullptr;
    bool m_alpha_lock = false;
    bool m_touch_lock = false;
    glm::mat4 m_mv{ 1 };
    glm::mat4 m_proj{ 1 };
    glm::vec4 m_box{ 0 };
    glm::vec4 m_vp{ 0 };
    glm::vec2 m_pan{ 0 };
    glm::vec2 m_size{ 0 };
    int m_width = 0;
    int m_height = 0;
    bool m_use_instanced = false;
    int m_current_layer_idx = 0;
    std::unique_ptr<Stroke> m_current_stroke;
    std::unique_ptr<Stroke> m_dual_stroke;
    bool m_show_tmp = false;
    std::vector<std::shared_ptr<Layer>> m_layers;
    int m_anim_frame = 0;
    Layer m_layers_merge;
    std::vector<glm::vec2> m_plane_shape[6]; // screen space projection of the plane
    glm::mat4 m_plane_unproject[6] = SIXPLETTE(glm::mat4(1));
    glm::vec3 m_plane_dir[6] = SIXPLETTE(glm::vec3(0));
    glm::vec4 m_dirty_box[6] = SIXPLETTE(glm::vec4(0));
    bool m_dirty_face[6] = SIXPLETTE(false);
    Layer m_smask; // selection mask
    bool m_smask_active = false;
    // mode 0=none, 1=free, 2=line
    int m_smask_mode = 0;
    RTT m_tmp[6];
    RTT m_tmp_dual[6];
    RTT m_mixer;
    float m_mixer_scale = 1;
    Texture2D m_tex[6];
    Texture2D m_tex2[6];
    RTT m_merge_rtt;
    Texture2D m_merge_tex;
    bool m_pick_ready[6] = SIXPLETTE(false);
    std::unique_ptr<glm::u8vec4[]> m_pick_data[6] = SIXPLETTE(nullptr);
    static glm::vec3 m_plane_origin[6];
    static glm::vec3 m_plane_normal[6];
    static glm::vec3 m_plane_tangent[6];
    static glm::mat4 m_plane_transform[6];
    glm::vec2 m_pattern_offset{ 0 };
    Sampler m_sampler;
    Sampler m_sampler_nearest;
    Sampler m_sampler_linear;
    Sampler m_sampler_brush;
    Sampler m_sampler_stencil;
    Sampler m_sampler_mix;
    glm::mat4 m_cam_rot = glm::mat4(1);
    glm::vec3 m_cam_pos{ 0 };
    float m_cam_fov = 85.f;
    const float m_cam_fov_min = 5.f;
    const float m_cam_fov_max = 150.f;
    glm::vec2 m_cur_pos{ 0 };

    std::shared_ptr<Brush> m_current_brush;

    static std::vector<CanvasMode*> modes[];
    std::vector<CanvasMode*>* m_mode = nullptr;
    kCanvasMode m_current_mode = kCanvasMode::Draw;
    std::function<void(kCanvasMode prev, kCanvasMode mode)> on_mode_changed;
    static void set_mode(kCanvasMode mode)
    {
        auto prev = I->m_current_mode;
        if (I->m_mode)
            for (auto& m : *I->m_mode)
                m->leave(mode);
        I->m_mode = &modes[(int)mode];
        I->m_current_mode = mode;
        if (I->m_mode)
            for (auto& m : *I->m_mode)
                m->enter(prev);
        if (I->on_mode_changed)
            I->on_mode_changed(prev, mode);
    }
    template <class T> static T* get_mode(int index = 0)
    {
        return dynamic_cast<T*>(modes[(int)I->m_current_mode][index]);
    }

    std::vector<LayerFrame::Snapshot> m_layers_snapshot;

    Canvas() { I = this; }
    ~Canvas() { destroy(); }
    void destroy();
    bool create(int width, int height);
    void resize(int width, int height);
    Layer& layer() { return *m_layers[m_current_layer_idx]; }
    std::shared_ptr<Layer> layer_with_id(uint32_t id);
    void layer_remove(int idx);
    void layer_add(std::string name, std::shared_ptr<Layer> layer = nullptr, int index = 0);
    void layer_order(int idx, int pos);
    void layer_merge(int source_idx, int dest_idx);
    int anim_duration() const noexcept;
    void anim_update() noexcept;
    void anim_goto_frame(int frame) noexcept;
    void anim_goto_next() noexcept;
    void anim_goto_prev() noexcept;
    void flood_fill(int layer, int plane, std::vector<glm::ivec2> pos, FloodData& plane_data,
        float threshold, glm::vec4 dest_color, std::unique_ptr<glm::vec4>& source_color);
    void stroke_start(glm::vec3 point, float pressure);
    void stroke_update(glm::vec3 point, float pressure);
    void stroke_draw_mix(const glm::vec2& bb_min, const glm::vec2& bb_sz);
    std::array<std::vector<vertex_t>, 6> stroke_draw_project(std::array<vertex_t, 4>& B, bool project_3d = false, glm::mat4 mv = glm::mat4(1)) const;
    // return rect {origin, size}
    glm::vec4 stroke_draw_samples(int i, std::vector<vertex_t>& P, bool copy_stroke_destination);
    std::vector<StrokeFrame> stroke_draw_compute(Stroke& stroke) const;
    void stroke_draw_live();
    void stroke_draw();
    void stroke_end();
    void stroke_cancel();
    void stroke_commit();
    void stroke_commit_timelapse();
    void draw_merge(bool draw_checkerboard, std::array<bool, 6> faces = SIXPLETTE(true));
    void clear(const glm::vec4& color = { 1, 1, 1, 0 });
    void clear_all();
    void pick_start();
    void pick_update(int plane);
    glm::vec4 pick_get(glm::vec2 canvas_loc);
    void pick_end();
    void snapshot_save();
    void snapshot_restore();
    void snap_history(const std::vector<int>& planes);
    void clear_context();
    void import_equirectangular(std::string file_path, std::shared_ptr<Layer> layer = nullptr);
    void import_equirectangular_thread(std::string file_path, std::shared_ptr<Layer> layer = nullptr, int frame = -1);
    void export_equirectangular(std::string file_path, std::function<void()> on_complete = nullptr);
    void export_equirectangular_thread(std::string file_path);
    void export_layers(std::string path, std::function<void()> on_complete = nullptr);
    void export_layers_thread(std::string path);
    void export_anim_frames(std::string path, std::function<void()> on_complete = nullptr);
    void export_anim_frames_thread(std::string path);
    void export_anim_mp4(std::string path, std::function<void()> on_complete = nullptr);
    void export_anim_mp4_thread(std::string path);
    void export_depth(std::string file_name, std::function<void()> on_complete = nullptr);
    void export_depth_thread(std::string file_name);
    void export_cube_faces(std::string file_name, std::function<void()> on_complete);
    void export_cube_faces_thread(std::string file_name);
    void project_save(std::function<void(bool)> on_complete = nullptr);
    void project_save(std::string file_path, std::function<void(bool)> on_complete = nullptr);
    bool project_save_thread(std::string file_path, bool show_progress);
    void project_open(std::string file_path, std::function<void(bool)> on_complete = nullptr);
    bool project_open_thread(std::string file_path);
    void inject_xmp(std::string jpg_path);
    Image thumbnail_generate(int w, int h);

private:
    void stroke_draw_pad_pass(
        const std::array<pp::panopainter::LegacyCanvasStrokePadFace, 6>& pad_faces,
        bool copy_stroke_destination,
        const std::array<pp::panopainter::LegacyCanvasStrokeTextureBinding, 1>& pad_destination_texture_binding,
        const pp::panopainter::LegacyCanvasStrokeTextureInputDispatch& pad_destination_texture_dispatch,
        const pp::renderer::Extent2D& stroke_extent,
        const glm::vec4& pad_color);
    void stroke_draw_pad_face_orchestration(
        const std::array<bool, 6>& box_dirty,
        const std::array<glm::vec4, 6>& box_face,
        bool copy_stroke_destination,
        const pp::renderer::Extent2D& stroke_extent,
        const glm::vec4& pad_color);
    void stroke_draw_pad_face_callback_body(
        const std::array<bool, 6>& box_dirty,
        const std::array<glm::vec4, 6>& box_face,
        bool copy_stroke_destination,
        const pp::renderer::Extent2D& stroke_extent,
        const glm::vec4& pad_color);
    void draw_merge_temporary_paint_branch(
        int layer_index,
        int plane_index,
        std::shared_ptr<Layer> layer,
        const Brush& brush,
        const glm::mat4& ortho);
    void draw_merge_branch_orchestration(
        int plane_index,
        int layer_index,
        const std::shared_ptr<Layer>& layer,
        const Brush& brush,
        const glm::mat4& ortho,
        bool use_blend,
        bool copy_blend_destination);
    void draw_merge_final_plane_composite(
        const glm::mat4& ortho,
        bool draw_checkerboard);
    pp::panopainter::LegacyStrokeFaceSamplePolygonExecutionRequest make_stroke_draw_samples_request(
        int face_index,
        std::vector<vertex_t>& polygon_vertices,
        bool copy_stroke_destination);
    pp::panopainter::LegacyCanvasStrokeTextureInputDispatch make_stroke_draw_samples_destination_texture_dispatch(
        int face_index);
    void stroke_draw_dual_pass(
        const std::vector<StrokeFrame>& frames_dual,
        const std::array<pp::panopainter::LegacyCanvasStrokeTextureBinding, 1>& dual_pass_texture_bindings,
        const pp::panopainter::LegacyCanvasStrokeTextureInputDispatch& dual_pass_brush_tip_dispatch,
        const pp::renderer::Extent2D& stroke_extent,
        const std::array<bool, 6>& include_dual_dirty,
        bool uses_pattern,
        bool copy_stroke_destination);
    pp::panopainter::LegacyCanvasStrokeMainPassExecutionRequest make_stroke_draw_main_pass_request(
        const Brush& brush,
        std::function<void()> bind_samplers,
        std::function<void()> execute_frame_pass,
        std::function<void()> unbind_samplers);
    pp::panopainter::LegacyCanvasStrokeDualPassRequest make_stroke_draw_dual_pass_request(
        const std::vector<StrokeFrame>& frames_dual,
        const std::array<pp::panopainter::LegacyCanvasStrokeTextureBinding, 1>& dual_pass_texture_bindings,
        const pp::panopainter::LegacyCanvasStrokeTextureInputDispatch& dual_pass_brush_tip_dispatch,
        const pp::renderer::Extent2D& stroke_extent,
        const std::array<bool, 6>& include_dual_dirty,
        bool uses_pattern,
        bool copy_stroke_destination);
    void stroke_draw_dual_pass_frame_pass(
        const std::vector<StrokeFrame>& frames_dual,
        const pp::renderer::Extent2D& stroke_extent,
        const std::array<bool, 6>& include_dual_dirty,
        bool copy_stroke_destination);

public:
    Image thumbnail_read(std::string file_path);
    void draw_objects(std::function<void(const glm::mat4& camera, const glm::mat4& proj, int i)>, int frame, bool save_history);
    void draw_objects(std::function<void(const glm::mat4& camera, const glm::mat4& proj, int i)>, Layer& layer, int frame, bool save_history);
    void draw_objects_direct(std::function<void(const glm::mat4& camera, const glm::mat4& proj, int i)>, Layer& layer, int frame);
    void point_unproject(glm::vec2 loc, glm::vec4 vp, glm::mat4 camera, glm::mat4 proj,
                         glm::vec3 &out_origin, glm::vec3 &out_dir);
    void point_unproject(glm::vec2 loc, glm::vec3 &out_origin, glm::vec3 &out_dir);
    glm::vec3 point_trace(glm::vec2 loc);
    bool point_trace(glm::vec2 loc, glm::vec3& ray_origin, glm::vec3& ray_dir,
                     glm::vec3& hit_pos, glm::vec2& fb_pos, glm::vec3& hit_normal, int& out_plane_id);
    bool point_trace_plane(glm::vec2 loc, glm::vec3& ray_origin, glm::vec3& ray_dir,
                     glm::vec3& hit_pos, glm::vec3& hit_normal, glm::vec2& hit_fb_pos, int plane_id);
    void project2Dpoints(std::vector<vertex_t>& vertices);
    glm::vec3 project2Dpoint(glm::vec2 pt);
    std::vector<glm::vec2> face_to_shape2D(int plane_index);
    void push_camera();
    void pop_camera();
    CameraData get_camera();
    void set_camera(const CameraData& c);
    void timelapse_reset_encoder() noexcept;
};
