#ifndef slic3r_GCodeProcessor_hpp_
#define slic3r_GCodeProcessor_hpp_

#include "libslic3r/GCodeReader.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/ExtrusionEntity.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/CustomGCode.hpp"
#include "libslic3r/Extruder.hpp"

#include <cstdint>
#include <array>
#include <vector>
#include <mutex>
#include <string>
#include <string_view>
#include <optional>

namespace Slic3r {

// slice warnings enum strings
#define NOZZLE_HRC_CHECKER                                          "the_actual_nozzle_hrc_smaller_than_the_required_nozzle_hrc"
#define BED_TEMP_TOO_HIGH_THAN_FILAMENT                             "bed_temperature_too_high_than_filament"
#define NOT_SUPPORT_TRADITIONAL_TIMELAPSE                           "not_support_traditional_timelapse"
#define NOT_GENERATE_TIMELAPSE                                      "not_generate_timelapse"
#define SMOOTH_TIMELAPSE_WITHOUT_PRIME_TOWER                        "smooth_timelapse_without_prime_tower"
#define LONG_RETRACTION_WHEN_CUT                                    "activate_long_retraction_when_cut"

    enum class EMoveType : unsigned char
    {
        Noop,
        Retract,
        Unretract,
        Seam,
        Tool_change,
        Color_change,
        Pause_Print,
        Custom_GCode,
        Travel,
        Wipe,
        Extrude,
        Count
    };

    enum SkipType
    {
        stTimelapse,
        stHeadWrapDetect,
        stOther,
        stNone
    };

    const std::unordered_map<std::string_view, SkipType> skip_type_map{
        {"timelapse", SkipType::stTimelapse},
        {"head_wrap_detect", SkipType::stHeadWrapDetect}
    };
    struct PrintEstimatedStatistics
    {
        enum class ETimeMode : unsigned char
        {
            Normal,
            Stealth,
            Count
        };

        struct Mode
        {
            float time;
            float prepare_time;
            std::vector<std::pair<CustomGCode::Type, std::pair<float, float>>> custom_gcode_times;
            std::vector<std::pair<EMoveType, float>> moves_times;
            std::vector<std::pair<ExtrusionRole, float>> roles_times;
            std::vector<float> layers_times;

            void reset() {
                time = 0.0f;
                prepare_time = 0.0f;
                custom_gcode_times.clear();
                custom_gcode_times.shrink_to_fit();
                moves_times.clear();
                moves_times.shrink_to_fit();
                roles_times.clear();
                roles_times.shrink_to_fit();
                layers_times.clear();
                layers_times.shrink_to_fit();
            }
        };

        std::vector<double>                                 volumes_per_color_change;
        std::map<size_t, double>                            model_volumes_per_extruder;
        std::map<size_t, double>                            wipe_tower_volumes_per_extruder;
        std::map<size_t, double>                            support_volumes_per_extruder;
        std::map<size_t, double>                            total_volumes_per_extruder;
        //QDS: the flush amount of every filament
        std::map<size_t, double>                            flush_per_filament;
        std::map<ExtrusionRole, std::pair<double, double>>  used_filaments_per_role;

        std::array<Mode, static_cast<size_t>(ETimeMode::Count)> modes;
        unsigned int                                        total_filament_changes;
        unsigned int                                        total_extruder_changes;

        PrintEstimatedStatistics() { reset(); }

        void reset() {
            for (auto m : modes) {
                m.reset();
            }
            volumes_per_color_change.clear();
            volumes_per_color_change.shrink_to_fit();
            wipe_tower_volumes_per_extruder.clear();
            model_volumes_per_extruder.clear();
            support_volumes_per_extruder.clear();
            total_volumes_per_extruder.clear();
            flush_per_filament.clear();
            used_filaments_per_role.clear();
            total_filament_changes = 0;
            total_extruder_changes = 0;
        }
    };

    struct ConflictResult
    {
        std::string        _objName1;
        std::string        _objName2;
        float             _height;
        const void *_obj1; // nullptr means wipe tower
        const void *_obj2;
        int                layer = -1;
        ConflictResult(const std::string &objName1, const std::string &objName2, float height, const void *obj1, const void *obj2)
            : _objName1(objName1), _objName2(objName2), _height(height), _obj1(obj1), _obj2(obj2)
        {}
        ConflictResult() = default;
    };

    using ConflictResultOpt = std::optional<ConflictResult>;

    struct GCodeCheckResult
    {
        int error_code = 0;   // 0 means succeed, 0001 printable area error, 0010 printable height error
        std::map<int, std::vector<std::pair<int, int>>> print_area_error_infos;   // printable_area  extruder_id to <filament_id - object_label_id> which cannot printed in this extruder
        std::map<int, std::vector<std::pair<int, int>>> print_height_error_infos;   // printable_height extruder_id to <filament_id - object_label_id> which cannot printed in this extruder
        void reset() {
            error_code = 0;
            print_area_error_infos.clear();
            print_height_error_infos.clear();
        }
    };

    struct FilamentPrintableResult
    {
        std::vector<int> conflict_filament;
        std::string plate_name;
        FilamentPrintableResult(){};
        FilamentPrintableResult(std::vector<int> &conflict_filament, std::string plate_name) : conflict_filament(conflict_filament), plate_name(plate_name) {}
        bool has_value(){
           return !conflict_filament.empty();
        };
    };

    struct GCodeProcessorResult
    {
        struct FilamentSequenceHash
        {
            uint64_t operator()(const std::vector<unsigned int>& layer_filament) const {
                uint64_t key = 0;
                for (auto& f : layer_filament)
                    key |= (uint64_t(1) << f);
                return key;
            }
        };
        ConflictResultOpt conflict_result;
        GCodeCheckResult  gcode_check_result;
        FilamentPrintableResult filament_printable_reuslt;

        struct SettingsIds
        {
            std::string print;
            std::vector<std::string> filament;
            std::string printer;

            void reset() {
                print.clear();
                filament.clear();
                printer.clear();
            }
        };

        struct MoveVertex
        {
            EMoveType type{ EMoveType::Noop };
            ExtrusionRole extrusion_role{ erNone };
            //QDS: arc move related data
            EMovePathType move_path_type{ EMovePathType::Noop_move };
            unsigned char extruder_id{ 0 };
            unsigned char cp_color_id{ 0 };

            unsigned int gcode_id{ 0 };
            float delta_extruder{ 0.0f }; // mm
            float feedrate{ 0.0f }; // mm/s
            float width{ 0.0f }; // mm
            float height{ 0.0f }; // mm
            float mm3_per_mm{ 0.0f };
            float fan_speed{ 0.0f }; // percentage
            float temperature{ 0.0f }; // Celsius degrees
            float layer_duration{ 0.0f }; // s (layer id before finalize)

            std::array<float, 2>time{ 0.f,0.f }; // prefix sum of time, assigned during finalize()

            Vec3f position{ Vec3f::Zero() }; // mm
            Vec3f arc_center_position{ Vec3f::Zero() };      // mm
            std::vector<Vec3f> interpolation_points;     // interpolation points of arc for drawing
            int  object_label_id{-1};
            float print_z{0.0f};

            float volumetric_rate() const { return feedrate * mm3_per_mm; }
            //QDS: new function to support arc move
            bool is_arc_move_with_interpolation_points() const {
                return (move_path_type == EMovePathType::Arc_move_ccw || move_path_type == EMovePathType::Arc_move_cw) && interpolation_points.size();
            }
            bool is_arc_move() const {
                return move_path_type == EMovePathType::Arc_move_ccw || move_path_type == EMovePathType::Arc_move_cw;
            }
        };

        struct SliceWarning {
            int         level;                  // 0: normal tips, 1: warning; 2: error
            std::string msg;                    // enum string
            std::string error_code;             // error code for studio
            std::vector<std::string> params;    // extra msg info
        };

        std::string filename;
        unsigned int id;
        std::vector<MoveVertex> moves;
        // Positions of ends of lines of the final G-code this->filename after TimeProcessor::post_process() finalizes the G-code.
        std::vector<size_t> lines_ends;
        Pointfs printable_area;
        //QDS: add bed exclude area
        Pointfs bed_exclude_area;
        std::vector<Pointfs> extruder_areas;
        std::vector<double> extruder_heights;
        //QDS: add toolpath_outside
        bool toolpath_outside;
        //QDS: add object_label_enabled
        bool label_object_enabled;
        //QDS : extra retraction when change filament,experiment func
        bool long_retraction_when_cut {0};
        int timelapse_warning_code {0};
        bool support_traditional_timelapse{true};
        float printable_height;
        SettingsIds settings_ids;
        size_t filaments_count;
        std::vector<std::string> extruder_colors;
        std::vector<float> filament_diameters;
        std::vector<int>   required_nozzle_HRC;
        std::vector<float> filament_densities;
        std::vector<float> filament_costs;
        std::vector<int> filament_vitrification_temperature;
        std::vector<int>   filament_maps;
        std::vector<int>   limit_filament_maps;
        PrintEstimatedStatistics print_statistics;
        std::vector<CustomGCode::Item> custom_gcode_per_print_z;
        std::vector<std::pair<float, std::pair<size_t, size_t>>> spiral_vase_layers;
        //QDS
        std::vector<SliceWarning> warnings;
        std::vector<NozzleType> nozzle_type;
        // first key stores filaments, second keys stores the layer ranges(enclosed) that use the filaments
        std::unordered_map<std::vector<unsigned int>, std::vector<std::pair<int, int>>,FilamentSequenceHash> layer_filaments;
        // first key stores `from` filament, second keys stores the `to` filament
        std::map<std::pair<int,int>, int > filament_change_count_map;

        std::unordered_map<SkipType, float> skippable_part_time;

        BedType bed_type = BedType::btCount;
#if ENABLE_GCODE_VIEWER_STATISTICS
        int64_t time{ 0 };
#endif // ENABLE_GCODE_VIEWER_STATISTICS
        void reset();

        //QDS: add mutex for protection of gcode result
        mutable std::mutex result_mutex;
        GCodeProcessorResult& operator=(const GCodeProcessorResult &other)
        {
            filename = other.filename;
            id = other.id;
            moves = other.moves;
            lines_ends = other.lines_ends;
            printable_area = other.printable_area;
            bed_exclude_area = other.bed_exclude_area;
            toolpath_outside = other.toolpath_outside;
            label_object_enabled = other.label_object_enabled;
            long_retraction_when_cut = other.long_retraction_when_cut;
            timelapse_warning_code = other.timelapse_warning_code;
            printable_height = other.printable_height;
            settings_ids = other.settings_ids;
            filaments_count = other.filaments_count;
            extruder_colors = other.extruder_colors;
            filament_diameters = other.filament_diameters;
            filament_densities = other.filament_densities;
            filament_costs = other.filament_costs;
            print_statistics = other.print_statistics;
            custom_gcode_per_print_z = other.custom_gcode_per_print_z;
            spiral_vase_layers = other.spiral_vase_layers;
            warnings = other.warnings;
            bed_type = other.bed_type;
            gcode_check_result = other.gcode_check_result;
            limit_filament_maps = other.limit_filament_maps;
            filament_printable_reuslt = other.filament_printable_reuslt;
            layer_filaments = other.layer_filaments;
            filament_change_count_map = other.filament_change_count_map;
            skippable_part_time = other.skippable_part_time;
#if ENABLE_GCODE_VIEWER_STATISTICS
            time = other.time;
#endif
            return *this;
        }
        void  lock() const { result_mutex.lock(); }
        void  unlock() const { result_mutex.unlock(); }
    };

    namespace ExtruderPreHeating
    {
        struct FilamentUsageBlock
        {
            int filament_id;
            unsigned int lower_gcode_id;
            unsigned int upper_gcode_id;  // [lower_gcode_id,upper_gcode_id) uses current filament , upper gcode id will be set after finding next block
            FilamentUsageBlock(int filament_id_, unsigned int lower_gcode_id_, unsigned int upper_gcode_id_) :filament_id(filament_id_), lower_gcode_id(lower_gcode_id_), upper_gcode_id(upper_gcode_id_) {}
        };

        /**
         * @brief Describle the usage of a exturder in a section
         *
         * The strucutre stores the start and end lines of the sections as well as
         * the filament used at the beginning and end of the section.
         * Post extrusion means the final extrusion before switching to the next extruder.
         *
         * Simplified GCode Flow:
         * 1.Extruder Change Block (ext0 switch to ext1)
         * 2.Extruder Usage Block  (use ext1 to print)
         * 3.Extruder Change Block (ext1 switch to ext0)
         * 4.Extruder Usage Block  (use ext0 to print)
         * 5.Extruder Change Block (ext0 switch to ex1)
         * ...
         *
         * So the construct of extruder usage block relys on two extruder change block
        */
        struct ExtruderUsageBlcok
        {
            int extruder_id = -1;
            unsigned int start_id = -1;
            unsigned int end_id = -1;
            int start_filament = -1;
            int end_filament = -1;
            unsigned int post_extrusion_start_id = -1;
            unsigned int post_extrusion_end_id = -1;

            void initialize_step_1(int extruder_id_, int start_id_, int start_filament_) {
                extruder_id = extruder_id_;
                start_id = start_id_;
                start_filament = start_filament_;
            };
            void initialize_step_2(int post_extrusion_start_id_) {
                post_extrusion_start_id = post_extrusion_start_id_;
            }
            void initialize_step_3(int end_id_, int end_filament_, int post_extrusion_end_id_) {
                end_id = end_id_;
                end_filament = end_filament_;
                post_extrusion_end_id = post_extrusion_end_id_;
            }
            void reset() {
                *this = ExtruderUsageBlcok();
            }
            ExtruderUsageBlcok() = default;
        };
    }


    class CommandProcessor {
    public:
        using command_handler_t = std::function<void(const GCodeReader::GCodeLine& line)>;
    private:
        struct TrieNode {
            command_handler_t handler{ nullptr };
            std::unordered_map<char, std::unique_ptr<TrieNode>> children;
            bool early_quit{ false }; // stop matching, trigger handle imediately
        };
    public:
        CommandProcessor();
        void register_command(const std::string& str, command_handler_t handler,bool early_quit = false);
        bool process_comand(std::string_view cmd, const GCodeReader::GCodeLine& line);
    private:
        std::unique_ptr<TrieNode> root;
    };


    class GCodeProcessor
    {
        static const std::vector<std::string> ReservedTags;
        static const std::vector<std::string> CustomTags;
    public:
        enum class ETags : unsigned char
        {
            Role,
            Wipe_Start,
            Wipe_End,
            Height,
            Width,
            Layer_Change,
            Color_Change,
            Pause_Print,
            Custom_Code,
            First_Line_M73_Placeholder,
            Last_Line_M73_Placeholder,
            Estimated_Printing_Time_Placeholder,
            Total_Layer_Number_Placeholder,
            Wipe_Tower_Start,
            Wipe_Tower_End,
            Used_Filament_Weight_Placeholder,
            Used_Filament_Volume_Placeholder,
            Used_Filament_Length_Placeholder,
            MachineStartGCodeEnd,
            MachineEndGCodeStart,
            NozzleChangeStart,
            NozzleChangeEnd
        };

        enum class CustomETags : unsigned char
        {
            FLUSH_START,
            FLUSH_END,
            VFLUSH_START,
            VFLUSH_END,
            SKIPPABLE_START,
            SKIPPABLE_END,
            SKIPPABLE_TYPE
        };

        static const std::string& reserved_tag(ETags tag) { return ReservedTags[static_cast<unsigned char>(tag)]; }
        static const std::string& custom_tags(CustomETags tag) { return CustomTags[static_cast<unsigned char>(tag)]; }
        // checks the given gcode for reserved tags and returns true when finding the 1st (which is returned into found_tag)
        static bool contains_reserved_tag(const std::string& gcode, std::string& found_tag);
        // checks the given gcode for reserved tags and returns true when finding any
        // (the first max_count found tags are returned into found_tag)
        static bool contains_reserved_tags(const std::string& gcode, unsigned int max_count, std::vector<std::string>& found_tag);

        static int get_gcode_last_filament(const std::string &gcode_str);
        static bool get_last_z_from_gcode(const std::string& gcode_str, double& z);
        static bool get_last_position_from_gcode(const std::string &gcode_str, Vec3f &pos);

        static const float Wipe_Width;
        static const float Wipe_Height;

        static bool s_IsQDTPrinter;

#if ENABLE_GCODE_VIEWER_DATA_CHECKING
        static const std::string Mm3_Per_Mm_Tag;
#endif // ENABLE_GCODE_VIEWER_DATA_CHECKING

    private:
        using AxisCoords = std::array<double, 4>;
        using ExtruderColors = std::vector<unsigned char>;
        using ExtruderTemps = std::vector<float>;

        enum class EUnits : unsigned char
        {
            Millimeters,
            Inches
        };

        enum class EPositioningType : unsigned char
        {
            Absolute,
            Relative
        };

        struct CachedPosition
        {
            AxisCoords position; // mm
            float feedrate; // mm/s

            void reset();
        };

        struct CpColor
        {
            unsigned char counter;
            unsigned char current;

            void reset();
        };

    public:
        struct FeedrateProfile
        {
            float entry{ 0.0f }; // mm/s
            float cruise{ 0.0f }; // mm/s
            float exit{ 0.0f }; // mm/s
        };

        struct Trapezoid
        {
            float accelerate_until{ 0.0f }; // mm
            float decelerate_after{ 0.0f }; // mm
            float cruise_feedrate{ 0.0f }; // mm/sec

            float acceleration_time(float entry_feedrate, float acceleration) const;
            float cruise_time() const;
            float deceleration_time(float distance, float acceleration) const;
            float cruise_distance() const;
        };

        struct TimeBlock
        {
            struct Flags
            {
                bool recalculate{ false };
                bool nominal_length{ false };
                bool prepare_stage{ false };
            };

            EMoveType move_type{ EMoveType::Noop };
            ExtrusionRole role{ erNone };
            SkipType skippable_type{ SkipType::stNone };
            unsigned int move_id{ 0 }; //  index of the related move vertex, will be assigned duraing gcode process
            unsigned int g1_line_id{ 0 };
            unsigned int layer_id{ 0 };
            float distance{ 0.0f }; // mm
            float acceleration{ 0.0f }; // mm/s^2
            float max_entry_speed{ 0.0f }; // mm/s
            float safe_feedrate{ 0.0f }; // mm/s
            Flags flags;
            FeedrateProfile feedrate_profile;
            Trapezoid trapezoid;

            // Calculates this block's trapezoid
            void calculate_trapezoid();

            float time() const;
        };


    private:
        struct TimeMachine
        {
            struct State
            {
                float feedrate; // mm/s
                float safe_feedrate; // mm/s
                //QDS: feedrate of X-Y-Z-E axis. But when the move is G2 and G3, X-Y will be
                //same value which means feedrate in X-Y plane.
                AxisCoords axis_feedrate; // mm/s
                AxisCoords abs_axis_feedrate; // mm/s

                //QDS: unit vector of enter speed and exit speed in x-y-z space.
                //For line move, there are same. For arc move, there are different.
                Vec3f enter_direction;
                Vec3f exit_direction;

                void reset();
            };

            struct CustomGCodeTime
            {
                bool needed;
                float cache;
                std::vector<std::pair<CustomGCode::Type, float>> times;

                void reset();
            };

            struct G1LinesCacheItem
            {
                unsigned int id;
                float elapsed_time;
            };

            bool enabled;
            float acceleration; // mm/s^2
            // hard limit for the acceleration, to which the firmware will clamp.
            float max_acceleration; // mm/s^2
            float retract_acceleration; // mm/s^2
            // hard limit for the acceleration, to which the firmware will clamp.
            float max_retract_acceleration; // mm/s^2
            float travel_acceleration; // mm/s^2
            // hard limit for the travel acceleration, to which the firmware will clamp.
            float max_travel_acceleration; // mm/s^2
            float extrude_factor_override_percentage;
            float time; // s
            struct StopTime
            {
                unsigned int g1_line_id;
                float elapsed_time;
            };
            std::vector<StopTime> stop_times;
            std::string line_m73_main_mask;
            std::string line_m73_stop_mask;
            State curr;
            State prev;
            CustomGCodeTime gcode_time;
            std::vector<TimeBlock> blocks;
            std::vector<G1LinesCacheItem> g1_times_cache;
            std::array<float, static_cast<size_t>(EMoveType::Count)> moves_time;
            std::array<float, static_cast<size_t>(ExtrusionRole::erCount)> roles_time;
            std::vector<float> layers_time;
            //QDS: prepare stage time before print model, including start gcode time and mostly same with start gcode time
            float prepare_time;

            // accept the time block and total time
            using block_handler_t = std::function<void(const TimeBlock&, const float)>;
            using AdditionalBufferBlock = std::pair<ExtrusionRole,float>;
            using AdditionalBuffer = std::vector<AdditionalBufferBlock>;
            AdditionalBuffer m_additional_time_buffer;

            AdditionalBuffer merge_adjacent_addtional_time_blocks(const AdditionalBuffer& buffer);

            void reset();

            /**
             * @brief Simulates firmware st_synchronize() call
             *
             * Adding additional time to the specified extrusion role's time block.The provided block handler
             * can process the block and the corresponding time (usually assigned to the move of the block).
             *
             * @param additional_time Addtional time to calculate
             * @param target_role Target extrusion role for addtional time.Default is none,means any role is ok.
             * @param block_handler Handler to set the processing logic for the block and its corresponding time.
             */
            void simulate_st_synchronize(float additional_time = 0.0f, ExtrusionRole target_role = ExtrusionRole::erNone, block_handler_t block_handler = block_handler_t());

            /**
             * @brief  Calculates the time for all blocks
             *
             * Computes the time for all blocks. The provided block handler can process each block and the
             * corresponding time (usually assigned to the move of the block).
             *
             * @param keep_last_n_blocks The number of last blocks to retain during calculation (default is 0).
             * @param additional_time  Additional time to calculate.
             * @param target_role Target extrusion role for addtional time.Default is none, means any role is ok.
             * @param block_handler Handler to set the processing logic for each block and its corresponding time.
             */
            void calculate_time(size_t keep_last_n_blocks = 0, float additional_time = 0.0f, ExtrusionRole target_role = ExtrusionRole::erNone, block_handler_t block_handler = block_handler_t());

            void handle_time_block(const TimeBlock& block, float time, int activate_machine_idx, GCodeProcessorResult& result);
        };

        struct UsedFilaments  // filaments per ColorChange
        {
            double color_change_cache;
            std::vector<double> volumes_per_color_change;

            double model_extrude_cache;
            std::map<size_t, double> model_volumes_per_filament;

            double wipe_tower_cache;
            std::map<size_t, double>wipe_tower_volumes_per_filament;

            double support_volume_cache;
            std::map<size_t, double>support_volumes_per_filament;

            //QDS: the flush amount of every filament
            std::map<size_t, double> flush_per_filament;

            double total_volume_cache;
            std::map<size_t, double>total_volumes_per_filament;

            double role_cache;
            std::map<ExtrusionRole, std::pair<double, double>> filaments_per_role;

            void reset();

            void increase_support_caches(double extruded_volume);
            void increase_model_caches(double extruded_volume);
            void increase_wipe_tower_caches(double extruded_volume);

            void process_color_change_cache();
            void process_model_cache(GCodeProcessor* processor);
            void process_wipe_tower_cache(GCodeProcessor* processor);
            void process_support_cache(GCodeProcessor* processor);
            void process_total_volume_cache(GCodeProcessor* processor);

            void update_flush_per_filament(size_t extrude_id, float flush_length);
            void process_role_cache(GCodeProcessor* processor);
            void process_caches(GCodeProcessor* processor);

            friend class GCodeProcessor;
        };

        struct TimeProcessContext
        {
            UsedFilaments used_filaments; // stores the accurate filament usage info
            std::vector<Extruder> filament_lists;
            std::vector<std::string> filament_types;
            std::vector<int> filament_maps; // map each filament to extruder
            std::vector<int> filament_nozzle_temp;
            std::vector<int> physical_extruder_map;

            size_t total_layer_num;
            std::vector<double> cooling_rate{ 2.f }; // Celsius degree per second
            std::vector<double> heating_rate{ 2.f }; // Celsius degree per second
            std::vector<int> pre_cooling_temp{ 0 };
            float inject_time_threshold{ 30.f }; // only active pre cooling & heating if time gap is bigger than threshold
            bool enable_pre_heating{ false };

            TimeProcessContext(
                const UsedFilaments& used_filaments_,
                const std::vector<Extruder>& filament_lists_,
                const std::vector<int>& filament_maps_,
                const std::vector<std::string>& filament_types_,
                const std::vector<int>& filament_nozzle_temp_,
                const std::vector<int>& physical_extruder_map_,
                const size_t total_layer_num_,
                const std::vector<double>& cooling_rate_,
                const std::vector<double>& heating_rate_,
                const std::vector<int>& pre_cooling_temp_,
                const float inject_time_threshold_,
                const bool  enable_pre_heating_
            ) :
                used_filaments(used_filaments_),
                filament_lists(filament_lists_),
                filament_maps(filament_maps_),
                filament_types(filament_types_),
                filament_nozzle_temp(filament_nozzle_temp_),
                physical_extruder_map(physical_extruder_map_),
                total_layer_num(total_layer_num_),
                cooling_rate(cooling_rate_),
                heating_rate(heating_rate_),
                pre_cooling_temp(pre_cooling_temp_),
                enable_pre_heating(enable_pre_heating_),
                inject_time_threshold(inject_time_threshold_)
            {
            }

        };

        struct TimeProcessor
        {
            enum InsertLineType
            {
                PlaceholderReplace,
                TimePredict,
                FilamentChangePredict,
                ExtruderChangePredict,
                PreCooling,
                PreHeating,
            };

            // first key is line id ,second key is content
            using InsertedLinesMap = std::map<unsigned int, std::vector<std::pair<std::string, InsertLineType>>>;

            struct Planner
            {
                // Size of the firmware planner queue. The old 8-bit Marlins usually just managed 16 trapezoidal blocks.
                // Let's be conservative and plan for newer boards with more memory.
                static constexpr size_t queue_size = 64;
                // The firmware recalculates last planner_queue_size trapezoidal blocks each time a new block is added.
                // We are not simulating the firmware exactly, we calculate a sequence of blocks once a reasonable number of blocks accumulate.
                static constexpr size_t refresh_threshold = queue_size * 4;
            };

            // extruder_id is currently used to correctly calculate filament load / unload times into the total print time.
            // This is currently only really used by the MK3 MMU2:
            // extruder_unloaded = true means no filament is loaded yet, all the filaments are parked in the MK3 MMU2 unit.
            bool extruder_unloaded;
            // allow to skip the lines M201/M203/M204/M205 generated by GCode::print_machine_envelope() for non-Normal time estimate mode
            bool machine_envelope_processing_enabled;
            MachineEnvelopeConfig machine_limits;
            // Additional load / unload times for a filament exchange sequence.
            float filament_load_times;
            float filament_unload_times;
            float extruder_change_times;
            float prepare_compensation_time;

            std::array<TimeMachine, static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count)> machines;

            void reset();

            // post process the file with the given filename to add remaining time lines M73
            // and updates moves' gcode ids accordingly
            void post_process(const std::string& filename, std::vector<GCodeProcessorResult::MoveVertex>& moves, std::vector<size_t>& lines_ends, const TimeProcessContext& context);
        private:
            void handle_offsets_of_first_process(
                const std::vector<std::pair<unsigned int, unsigned int>>& offsets,
                std::vector<GCodeProcessorResult::MoveVertex>& moves,
                std::vector<ExtruderPreHeating::FilamentUsageBlock>& filament_blocks,
                std::vector<ExtruderPreHeating::ExtruderUsageBlcok>& extruder_blocks,
                std::vector<std::pair<unsigned int, unsigned int>>& skippable_blocks,
                unsigned int& machine_start_gcode_end_line_id,
                unsigned int& machine_end_gcode_start_line_id
            );

            void handle_offsets_of_second_process(
                const InsertedLinesMap& inserted_operation_lines,
                std::vector<GCodeProcessorResult::MoveVertex>& moves
            );
        };

        class PreCoolingInjector {
        public:
            struct ExtruderFreeBlock {
                unsigned int free_lower_gcode_id;
                unsigned int free_upper_gcode_id;
                unsigned int partial_free_lower_id; // stores the range of extrusion in wipe tower. Without wipetower, partial free lower_id and upper id will be same as free lower id
                unsigned int partial_free_upper_id;
                int last_filament_id;
                int next_filament_id;
                int extruder_id;
            };

            void process_pre_cooling_and_heating(TimeProcessor::InsertedLinesMap& inserted_operation_lines);
            void build_extruder_free_blocks(const std::vector<ExtruderPreHeating::FilamentUsageBlock>& filament_usage_blocks, const std::vector<ExtruderPreHeating::ExtruderUsageBlcok>& extruder_usage_blocks);

            PreCoolingInjector(
                const std::vector<GCodeProcessorResult::MoveVertex>& moves_,
                const std::vector<std::string>& filament_types_,
                const std::vector<int>& filament_maps_,
                const std::vector<int>& filament_nozzle_temps_,
                const std::vector<int>& physical_extruder_map_,
                int valid_machine_id_,
                float inject_time_threshold_,
                const std::vector<int> & pre_cooling_temp_,
                const std::vector<double>& cooling_rate_,
                const std::vector<double>& heating_rate_,
                const std::vector<std::pair<unsigned int,unsigned int>>& skippable_blocks_,
                unsigned int machine_start_gcode_end_id_,
                unsigned int machine_end_gcode_start_id_
            ) :
                moves(moves_),
                filament_types(filament_types_),
                filament_maps(filament_maps_),
                filament_nozzle_temps(filament_nozzle_temps_),
                physical_extruder_map(physical_extruder_map_),
                valid_machine_id(valid_machine_id_),
                inject_time_threshold(inject_time_threshold_),
                filament_pre_cooling_temps(pre_cooling_temp_),
                cooling_rate(cooling_rate_),
                heating_rate(heating_rate_),
                skippable_blocks(skippable_blocks_),
                machine_start_gcode_end_id(machine_start_gcode_end_id_),
                machine_end_gcode_start_id(machine_end_gcode_start_id_)
            {
            }

        private:
            std::vector<ExtruderFreeBlock> m_extruder_free_blocks;
            const std::vector<GCodeProcessorResult::MoveVertex>& moves;
            const std::vector<std::string>& filament_types;
            const std::vector<int>& filament_maps;
            const std::vector<int>& filament_nozzle_temps;
            const std::vector<int>& physical_extruder_map;
            const int valid_machine_id;
            const float inject_time_threshold;
            const std::vector<double>& cooling_rate;
            const std::vector<double>& heating_rate;
            const std::vector<int>& filament_pre_cooling_temps; // target cooling temp during post extrusion
            const std::vector<std::pair<unsigned int, unsigned int>>& skippable_blocks;
            const unsigned int machine_start_gcode_end_id;
            const unsigned int machine_end_gcode_start_id;

            void inject_cooling_heating_command(
                TimeProcessor::InsertedLinesMap& inserted_operation_lines,
                const ExtruderFreeBlock& free_block,
                float curr_temp,
                float target_temp,
                bool pre_cooling,
                bool pre_heating
            );

            void build_by_filament_blocks(const std::vector<ExtruderPreHeating::FilamentUsageBlock>& filament_usage_blocks);
            void build_by_extruder_blocks(const std::vector<ExtruderPreHeating::ExtruderUsageBlcok>& extruder_usage_blocks);
        };


    public:
        class SeamsDetector
        {
            bool m_active{ false };
            std::optional<Vec3f> m_first_vertex;

        public:
            void activate(bool active) {
                if (m_active != active) {
                    m_active = active;
                    if (m_active)
                        m_first_vertex.reset();
                }
            }

            std::optional<Vec3f> get_first_vertex() const { return m_first_vertex; }
            void set_first_vertex(const Vec3f& vertex) { m_first_vertex = vertex; }

            bool is_active() const { return m_active; }
            bool has_first_vertex() const { return m_first_vertex.has_value(); }
        };

        // Helper class used to fix the z for color change, pause print and
        // custom gcode markes
        class OptionsZCorrector
        {
            GCodeProcessorResult& m_result;
            std::optional<size_t> m_move_id;
            std::optional<size_t> m_custom_gcode_per_print_z_id;

        public:
            explicit OptionsZCorrector(GCodeProcessorResult& result) : m_result(result) {
            }

            void set() {
                m_move_id = m_result.moves.size() - 1;
                m_custom_gcode_per_print_z_id = m_result.custom_gcode_per_print_z.size() - 1;
            }

            void update(float height) {
                if (!m_move_id.has_value() || !m_custom_gcode_per_print_z_id.has_value())
                    return;

                const Vec3f position = m_result.moves.back().position;

                GCodeProcessorResult::MoveVertex& move = m_result.moves.emplace_back(m_result.moves[*m_move_id]);
                move.position = position;
                move.height = height;
                m_result.moves.erase(m_result.moves.begin() + *m_move_id);
                m_result.custom_gcode_per_print_z[*m_custom_gcode_per_print_z_id].print_z = position.z();
                reset();
            }

            void reset() {
                m_move_id.reset();
                m_custom_gcode_per_print_z_id.reset();
            }
        };

#if ENABLE_GCODE_VIEWER_DATA_CHECKING
        struct DataChecker
        {
            struct Error
            {
                float value;
                float tag_value;
                ExtrusionRole role;
            };

            std::string type;
            float threshold{ 0.01f };
            float last_tag_value{ 0.0f };
            unsigned int count{ 0 };
            std::vector<Error> errors;

            DataChecker(const std::string& type, float threshold)
                : type(type), threshold(threshold)
            {}

            void update(float value, ExtrusionRole role) {
                if (role != erCustom) {
                    ++count;
                    if (last_tag_value != 0.0f) {
                        if (std::abs(value - last_tag_value) / last_tag_value > threshold)
                            errors.push_back({ value, last_tag_value, role });
                    }
                }
            }

            void reset() { last_tag_value = 0.0f; errors.clear(); count = 0; }

            std::pair<float, float> get_min() const {
                float delta_min = FLT_MAX;
                float perc_min = 0.0f;
                for (const Error& e : errors) {
                    if (delta_min > e.value - e.tag_value) {
                        delta_min = e.value - e.tag_value;
                        perc_min = 100.0f * delta_min / e.tag_value;
                    }
                }
                return { delta_min, perc_min };
            }

            std::pair<float, float> get_max() const {
                float delta_max = -FLT_MAX;
                float perc_max = 0.0f;
                for (const Error& e : errors) {
                    if (delta_max < e.value - e.tag_value) {
                        delta_max = e.value - e.tag_value;
                        perc_max = 100.0f * delta_max / e.tag_value;
                    }
                }
                return { delta_max, perc_max };
            }

            void output() const {
                if (!errors.empty()) {
                    std::cout << type << ":\n";
                    std::cout << "Errors: " << errors.size() << " (" << 100.0f * float(errors.size()) / float(count) << "%)\n";
                    auto [min, perc_min] = get_min();
                    auto [max, perc_max] = get_max();
                    std::cout << "min: " << min << "(" << perc_min << "%) - max: " << max << "(" << perc_max << "%)\n";
                }
            }
        };
#endif // ENABLE_GCODE_VIEWER_DATA_CHECKING

    private:
        CommandProcessor m_command_processor;
        GCodeReader m_parser;
        EUnits m_units;
        EPositioningType m_global_positioning_type;
        EPositioningType m_e_local_positioning_type;
        std::vector<Vec3f> m_extruder_offsets;
        GCodeFlavor m_flavor;
        std::vector<float> m_nozzle_volume;
        AxisCoords m_start_position; // mm
        AxisCoords m_end_position; // mm
        AxisCoords m_origin; // mm
        CachedPosition m_cached_position;
        bool m_wiping;
        bool m_flushing; // mark a section with real flush
        bool m_virtual_flushing; // mark a section with virtual flush, only for statistics
        bool m_wipe_tower;
        bool m_skippable;
        SkipType m_skippable_type;
        int m_object_label_id{-1};
        float m_print_z{0.0f};
        std::vector<float> m_remaining_volume;
        std::vector<Extruder> m_filament_lists;
        std::vector<int> m_filament_nozzle_temp;
        std::vector<std::string> m_filament_types;
        std::vector<double> m_hotend_cooling_rate{ 2.f };
        std::vector<double> m_hotend_heating_rate{ 2.f };
        std::vector<int> m_filament_pre_cooling_temp{ 0 };
        float m_enable_pre_heating{ false };
        std::vector<int> m_physical_extruder_map;

        //QDS: x, y offset for gcode generated
        double          m_x_offset{ 0 };
        double          m_y_offset{ 0 };
        //QDS: arc move related data
        EMovePathType m_move_path_type{ EMovePathType::Noop_move };
        Vec3f m_arc_center{ Vec3f::Zero() };    // mm
        std::vector<Vec3f> m_interpolation_points;

        unsigned int m_line_id;
        unsigned int m_last_line_id;
        float m_feedrate; // mm/s
        float m_width; // mm
        float m_height; // mm
        float m_forced_width; // mm
        float m_forced_height; // mm
        float m_mm3_per_mm;
        float m_fan_speed; // percentage
        ExtrusionRole m_extrusion_role;
        std::vector<int> m_filament_maps;
        std::vector<unsigned char> m_last_filament_id;
        std::vector<unsigned char> m_filament_id;
        unsigned char m_extruder_id;
        ExtruderColors m_extruder_colors;
        ExtruderTemps m_extruder_temps;
        int m_highest_bed_temp;
        float m_extruded_last_z;
        float m_first_layer_height; // mm
        float m_zero_layer_height; // mm
        bool m_processing_start_custom_gcode;
        unsigned int m_g1_line_id;
        unsigned int m_layer_id;
        CpColor m_cp_color;
        SeamsDetector m_seams_detector;
        OptionsZCorrector m_options_z_corrector;
        size_t m_last_default_color_id;
        bool m_detect_layer_based_on_tag {false};
        int m_seams_count;
        bool m_measure_g29_time {false};
#if ENABLE_GCODE_VIEWER_STATISTICS
        std::chrono::time_point<std::chrono::high_resolution_clock> m_start_time;
#endif // ENABLE_GCODE_VIEWER_STATISTICS

        enum class EProducer
        {
            Unknown,
            QIDIStudio,
            Slic3rPE,
            Slic3r,
            SuperSlicer,
            Cura,
            Simplify3D,
            CraftWare,
            ideaMaker,
            KissSlicer,
            QIDISlicer,
            BambuStudio
        };

        static const std::vector<std::pair<GCodeProcessor::EProducer, std::string>> Producers;
        EProducer m_producer;

        TimeProcessor m_time_processor;
        UsedFilaments m_used_filaments;

        GCodeProcessorResult m_result;
        static unsigned int s_result_id;

#if ENABLE_GCODE_VIEWER_DATA_CHECKING
        DataChecker m_mm3_per_mm_compare{ "mm3_per_mm", 0.01f };
        DataChecker m_height_compare{ "height", 0.01f };
        DataChecker m_width_compare{ "width", 0.01f };
#endif // ENABLE_GCODE_VIEWER_DATA_CHECKING

    public:
        GCodeProcessor();
        void init_filament_maps_and_nozzle_type_when_import_only_gcode();
        // check whether the gcode path meets the filament_map grouping requirements
        bool check_multi_extruder_gcode_valid(const std::vector<Polygons> &unprintable_areas,
                                              const std::vector<double>   &printable_heights,
                                              const std::vector<int>      &filament_map,
                                              const std::vector<std::set<int>>& unprintable_filament_types );
        void apply_config(const PrintConfig& config);

        DynamicConfig export_config_for_render() const;
        void set_filaments(const std::vector<Extruder>&filament_lists) { m_filament_lists=filament_lists;}

        void enable_stealth_time_estimator(bool enabled);
        bool is_stealth_time_estimator_enabled() const {
            return m_time_processor.machines[static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Stealth)].enabled;
        }
        void enable_machine_envelope_processing(bool enabled) { m_time_processor.machine_envelope_processing_enabled = enabled; }
        void reset();

        const GCodeProcessorResult& get_result() const { return m_result; }
        GCodeProcessorResult& result() { return m_result; }
        GCodeProcessorResult&& extract_result() { return std::move(m_result); }

        // Load a G-code into a stand-alone G-code viewer.
        // throws CanceledException through print->throw_if_canceled() (sent by the caller as callback).
        void process_file(const std::string& filename, std::function<void()> cancel_callback = nullptr);

        // Streaming interface, for processing G-codes just generated by PrusaSlicer in a pipelined fashion.
        void initialize(const std::string& filename);
        void process_buffer(const std::string& buffer);
        void finalize(bool post_process);

        float get_time(PrintEstimatedStatistics::ETimeMode mode) const;
        float get_prepare_time(PrintEstimatedStatistics::ETimeMode mode) const;
        std::string get_time_dhm(PrintEstimatedStatistics::ETimeMode mode) const;
        std::vector<std::pair<CustomGCode::Type, std::pair<float, float>>> get_custom_gcode_times(PrintEstimatedStatistics::ETimeMode mode, bool include_remaining) const;

        std::vector<std::pair<EMoveType, float>> get_moves_time(PrintEstimatedStatistics::ETimeMode mode) const;
        std::vector<std::pair<ExtrusionRole, float>> get_roles_time(PrintEstimatedStatistics::ETimeMode mode) const;
        std::vector<float> get_layers_time(PrintEstimatedStatistics::ETimeMode mode) const;

        //QDS: set offset for gcode writer
        void set_xy_offset(double x, double y) { m_x_offset = x; m_y_offset = y; }
        // Orca: if true, only change new layer if ETags::Layer_Change occurs
        // otherwise when we got a lift of z during extrusion, a new layer will be added
        void detect_layer_based_on_tag(bool enabled) {
            m_detect_layer_based_on_tag = enabled;
        }

    private:
        void register_commands();
        void apply_config(const DynamicPrintConfig& config);
        void apply_config_simplify3d(const std::string& filename);
        void apply_config_superslicer(const std::string& filename);
        void process_gcode_line(const GCodeReader::GCodeLine& line, bool producers_enabled);

        // Process tags embedded into comments
        void process_tags(const std::string_view comment, bool producers_enabled);
        bool process_producers_tags(const std::string_view comment);
        bool process_qidislicer_tags(const std::string_view comment);
        bool process_cura_tags(const std::string_view comment);
        bool process_simplify3d_tags(const std::string_view comment);
        bool process_craftware_tags(const std::string_view comment);
        bool process_ideamaker_tags(const std::string_view comment);
        bool process_kissslicer_tags(const std::string_view comment);

        bool detect_producer(const std::string_view comment);

        // Move
        void process_G0(const GCodeReader::GCodeLine& line);
        void process_G1(const GCodeReader::GCodeLine& line);
        void process_G2_G3(const GCodeReader::GCodeLine& line);

        void process_VG1(const GCodeReader::GCodeLine& line);

        // QDS: handle delay command
        void process_G4(const GCodeReader::GCodeLine& line);

        // Retract
        void process_G10(const GCodeReader::GCodeLine& line);

        // Unretract
        void process_G11(const GCodeReader::GCodeLine& line);

        // Set Units to Inches
        void process_G20(const GCodeReader::GCodeLine& line);

        // Set Units to Millimeters
        void process_G21(const GCodeReader::GCodeLine& line);

        // Firmware controlled Retract
        void process_G22(const GCodeReader::GCodeLine& line);

        // Firmware controlled Unretract
        void process_G23(const GCodeReader::GCodeLine& line);

        // Move to origin
        void process_G28(const GCodeReader::GCodeLine& line);

        // QDS
        void process_G29(const GCodeReader::GCodeLine& line);

        // Set to Absolute Positioning
        void process_G90(const GCodeReader::GCodeLine& line);

        // Set to Relative Positioning
        void process_G91(const GCodeReader::GCodeLine& line);

        // Set Position
        void process_G92(const GCodeReader::GCodeLine& line);

        // Sleep or Conditional stop
        void process_M1(const GCodeReader::GCodeLine& line);

        // Set extruder to absolute mode
        void process_M82(const GCodeReader::GCodeLine& line);

        // Set extruder to relative mode
        void process_M83(const GCodeReader::GCodeLine& line);

        // Set extruder temperature
        void process_M104(const GCodeReader::GCodeLine& line);

        // Process virtual command of M104, in order to help gcodeviewer work
        void process_VM104(const GCodeReader::GCodeLine& line);

        // Process virtual command of M109, in order to help gcodeviewer work
        void process_VM109(const GCodeReader::GCodeLine& line);

        // Set fan speed
        void process_M106(const GCodeReader::GCodeLine& line);

        // Disable fan
        void process_M107(const GCodeReader::GCodeLine& line);

        // Set tool (Sailfish)
        void process_M108(const GCodeReader::GCodeLine& line);

        // Set extruder temperature and wait
        void process_M109(const GCodeReader::GCodeLine& line);

        // Recall stored home offsets
        void process_M132(const GCodeReader::GCodeLine& line);

        // Set tool (MakerWare)
        void process_M135(const GCodeReader::GCodeLine& line);

        //QDS: Set bed temperature
        void process_M140(const GCodeReader::GCodeLine& line);

        //QDS: wait bed temperature
        void process_M190(const GCodeReader::GCodeLine& line);

        //QDS: wait chamber temperature
        void process_M191(const GCodeReader::GCodeLine& line);

        // Set max printing acceleration
        void process_M201(const GCodeReader::GCodeLine& line);

        // Set maximum feedrate
        void process_M203(const GCodeReader::GCodeLine& line);

        // Set default acceleration
        void process_M204(const GCodeReader::GCodeLine& line);

        // Advanced settings
        void process_M205(const GCodeReader::GCodeLine& line);

        // Klipper SET_VELOCITY_LIMIT
        void process_SET_VELOCITY_LIMIT(const GCodeReader::GCodeLine& line);

        // Set extrude factor override percentage
        void process_M221(const GCodeReader::GCodeLine& line);

        // QDS: handle delay command. M400 is defined by QDT only
        void process_M400(const GCodeReader::GCodeLine& line);

        // Repetier: Store x, y and z position
        void process_M401(const GCodeReader::GCodeLine& line);

        // Repetier: Go to stored position
        void process_M402(const GCodeReader::GCodeLine& line);

        // Set allowable instantaneous speed change
        void process_M566(const GCodeReader::GCodeLine& line);

        // Unload the current filament into the MK3 MMU2 unit at the end of print.
        void process_M702(const GCodeReader::GCodeLine& line);

        void process_SYNC(const GCodeReader::GCodeLine& line);

        // Processes T line (Select Tool)
        void process_T(const GCodeReader::GCodeLine& line);
        void process_T(const std::string_view command);
        void process_M1020(const GCodeReader::GCodeLine &line);

        void process_M622(const GCodeReader::GCodeLine &line);
        void process_M623(const GCodeReader::GCodeLine &line);

        void process_filament_change(int id);
        //QDS: different path_type is only used for arc move
        void store_move_vertex(EMoveType type, EMovePathType path_type = EMovePathType::Noop_move);

        void set_extrusion_role(ExtrusionRole role);
        void set_skippable_type(const std::string_view type);

        float minimum_feedrate(PrintEstimatedStatistics::ETimeMode mode, float feedrate) const;
        float minimum_travel_feedrate(PrintEstimatedStatistics::ETimeMode mode, float feedrate) const;
        float get_axis_max_feedrate(PrintEstimatedStatistics::ETimeMode mode, Axis axis, int extruder_id) const;
        float get_axis_max_acceleration(PrintEstimatedStatistics::ETimeMode mode, Axis axis, int extruder_id) const;
        float get_axis_max_jerk(PrintEstimatedStatistics::ETimeMode mode, Axis axis) const;
        Vec3f get_xyz_max_jerk(PrintEstimatedStatistics::ETimeMode mode) const;
        float get_retract_acceleration(PrintEstimatedStatistics::ETimeMode mode) const;
        void  set_retract_acceleration(PrintEstimatedStatistics::ETimeMode mode, float value);
        float get_acceleration(PrintEstimatedStatistics::ETimeMode mode) const;
        void  set_acceleration(PrintEstimatedStatistics::ETimeMode mode, float value);
        float get_travel_acceleration(PrintEstimatedStatistics::ETimeMode mode) const;
        void  set_travel_acceleration(PrintEstimatedStatistics::ETimeMode mode, float value);
        float get_filament_load_time(size_t extruder_id);
        float get_filament_unload_time(size_t extruder_id);
        float get_extruder_change_time(size_t extruder_id);
        int   get_filament_vitrification_temperature(size_t extrude_id);
        void process_custom_gcode_time(CustomGCode::Type code);
        void process_filaments(CustomGCode::Type code);

        // Simulates firmware st_synchronize() call
        void simulate_st_synchronize(float additional_time = 0.0f, ExtrusionRole target_role =ExtrusionRole::erNone);

        void update_estimated_times_stats();
        //QDS:
        void update_slice_warnings();

        // get current used filament
        int get_filament_id(bool force_initialize = true) const;
        // get last used filament in the same extruder with current filament
        int get_last_filament_id(bool force_initialize = true) const;
        //get current used extruder
        int get_extruder_id(bool force_initialize = true)const;
   };

} /* namespace Slic3r */

#endif /* slic3r_GCodeProcessor_hpp_ */


