///|/ Copyright (c) Prusa Research 2022 - 2023 Pavel Mikuš @Godrak
///|/
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
///|/
#ifndef SRC_LIBSLIC3R_SUPPORTABLEISSUESSEARCH_HPP_
#define SRC_LIBSLIC3R_SUPPORTABLEISSUESSEARCH_HPP_

#include <boost/log/trivial.hpp>
#include <cstddef>
#include <vector>
#include <algorithm>
#include <optional>
#include <string>
#include <tuple>
#include <utility>

#include "Layer.hpp"
#include "Line.hpp"
#include "PrintBase.hpp"
#include "PrintConfig.hpp"
#include "libslic3r/ExPolygon.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/Polygon.hpp"
#include "libslic3r/Polyline.hpp"
#include "libslic3r/libslic3r.h"

namespace Slic3r {
class ExtrusionEntity;
class ExtrusionEntityCollection;
class Layer;
class PrintObject;
class SupportLayer;

namespace SupportSpotsGenerator {

struct Params
{
    Params(
        const std::vector<std::string> &filament_types, float max_acceleration, int raft_layers_count, BrimType brim_type, float brim_width)
        : max_acceleration(max_acceleration), raft_layers_count(raft_layers_count), brim_type(brim_type), brim_width(brim_width)
    {
        if (filament_types.size() > 1) {
            BOOST_LOG_TRIVIAL(warning)
                << "SupportSpotsGenerator does not currently handle different materials properly, only first will be used";
        }
        if (filament_types.empty() || filament_types[0].empty()) {
            BOOST_LOG_TRIVIAL(error) << "SupportSpotsGenerator error: empty filament_type";
            filament_type = std::string("PLA");
        } else {
            filament_type = filament_types[0];
            BOOST_LOG_TRIVIAL(debug) << "SupportSpotsGenerator: applying filament type: " << filament_type;
        }
    }

    // the algorithm should use the following units for all computations: distance [mm], mass [g], time [s], force [g*mm/s^2]
    const float bridge_distance = 16.0f; // mm
    const float max_acceleration; // mm/s^2 ; max acceleration of object in XY -- should be applicable only to printers with bed slinger, 
                                  // however we do not have such info yet. The force is usually small anyway, so not such a big deal to include it everytime
    const int raft_layers_count;
    std::string filament_type;

    BrimType brim_type;
    const float brim_width;

    const std::pair<float,float> malformation_distance_factors = std::pair<float, float> { 0.2, 1.1 };
    const float max_curled_height_factor = 10.0f;
    const float curling_tolerance_limit = 0.1f;

    const float min_distance_between_support_points = 3.0f; //mm
    const float support_points_interface_radius = 1.5f; // mm
    const float min_distance_to_allow_local_supports = 1.0f; //mm

    const float gravity_constant = 9806.65f; // mm/s^2; gravity acceleration on Earth's surface, algorithm assumes that printer is in upwards position.
    const double filament_density = 1.25e-3f; // g/mm^3  ; Common filaments are very lightweight, so precise number is not that important
    const double material_yield_strength = 33.0f * 1e6f; // (g*mm/s^2)/mm^2; 33 MPa is yield strength of ABS, which has the lowest yield strength from common materials.
    const float standard_extruder_conflict_force = 10.0f * gravity_constant; // force that can occasionally push the model due to various factors (filament leaks, small curling, ... );
    const float malformations_additive_conflict_extruder_force = 65.0f * gravity_constant; // for areas with possible high layered curled filaments

    // MPa * 1e^6 = (g*mm/s^2)/mm^2 = g/(mm*s^2); yield strength of the bed surface
    double get_bed_adhesion_yield_strength() const {
        if (raft_layers_count > 0) {
            return get_support_spots_adhesion_strength() * 2.0;
        }

        if (filament_type == "PLA") {
            return 0.02 * 1e6;
        } else if (filament_type == "PET" || filament_type == "PETG") {
            return 0.3 * 1e6;
        } else if (filament_type == "ABS" || filament_type == "ASA") {
            return 0.1 * 1e6; //TODO do measurements
        } else { //PLA default value - defensive approach, PLA has quite low adhesion
            return 0.02 * 1e6;
        }
    }

    double get_support_spots_adhesion_strength() const {
         return 0.016f * 1e6; 
    }
};

enum class SupportPointCause { 
    LongBridge, // point generated on bridge and straight perimeter extrusion longer than the allowed length 
    FloatingBridgeAnchor, // point generated on unsupported bridge endpoint
    FloatingExtrusion, // point generated on extrusion that does not hold on its own
    SeparationFromBed, // point generated for object parts that are connected to the bed, but the area is too small and there is a risk of separation (brim may help)
    UnstableFloatingPart, // point generated for object parts not connected to the bed, holded only by the other support points (brim will not help here)
    WeakObjectPart // point generated when some part of the object is too weak to hold the upper part and may break (imagine hourglass)
    };

// The support points can be sorted into two groups
// 1. Local extrusion support for extrusions that are printed in the air and would not
//    withstand on their own (too long bridges, sharp turns in large overhang, concave bridge holes, etc.)
//    These points have negative force (-EPSILON) and Vec2f::Zero() direction
//    The algorithm still expects that these points will be supported and accounts for them in the global stability check.
// 2. Global stability support points are generated at each spot, where the algorithm detects that extruding the current line
//    may cause separation of the object part from the bed and/or its support spots or crack in the weak connection of the object parts.
//    The generated point's direction is the estimated falling direction of the object part, and the force is equal to te difference
//    between forces that destabilize the object (extruder conflicts with curled filament, weight if instable center of mass, bed movements etc)
//    and forces that stabilize the object (bed adhesion, other support spots adhesion, weight if stable center of mass).
//    Note that the force is only the difference - the amount needed to stabilize the object again.
struct SupportPoint
{
    SupportPoint(SupportPointCause cause, const Vec3f &position, float spot_radius)
        : cause(cause), position(position), spot_radius(spot_radius)
    {}

    bool is_local_extrusion_support() const
    {
        return cause == SupportPointCause::LongBridge || cause == SupportPointCause::FloatingExtrusion;
    }
    bool is_global_object_support() const { return !is_local_extrusion_support(); }

    SupportPointCause cause; // reason why this support point was generated. Used for the user alerts
    // position is in unscaled coords. The z coordinate is aligned with the layers bottom_z coordiantes
    Vec3f position;
    // Expected spot size. The support point strength is calculated from the area defined by this value.
    // Currently equal to the support_points_interface_radius parameter above
    float spot_radius;
};

using SupportPoints = std::vector<SupportPoint>;

struct Malformations {
    std::vector<Lines> layers; //for each layer
};

struct PartialObject
{
    PartialObject(Vec3f centroid, float volume, bool connected_to_bed)
        : centroid(centroid), volume(volume), connected_to_bed(connected_to_bed)
    {}

    Vec3f centroid;
    float volume;
    bool  connected_to_bed;
};


/**
 * Unsacled values of integrals over a polygonal domain.
 */
class Integrals{
  public:
    /**
     * Construct integral x_i int x_i^2 (i=1,2), xy and integral 1 (area).
     *
     * @param polygons List of polygons specifing the domain.
     */
    explicit Integrals(const Polygons& polygons);
    explicit Integrals(const Polygon& polygon);
    /**
     * Construct integral x_i int x_i^2 (i=1,2), xy and integral 1 (area) over
     * a set of rectangles defined by a "thick" polyline.
     */
    explicit Integrals(const Polylines& polylines, const std::vector<float>& widths);

    // TODO refactor and delete the default constructor
    Integrals() = default;
    Integrals(float area, Vec2f x_i, Vec2f x_i_squared, float xy);

    float area{};
    Vec2f x_i{Vec2f::Zero()};
    Vec2f x_i_squared{Vec2f::Zero()};
    float xy{};

private:
    void add(const Integrals& other);
};

Integrals operator+(const Integrals& a, const Integrals& b);

float compute_second_moment(
    const Integrals& integrals,
    const Vec2f& axis_direction
);

class ExtrusionLine
{
public:
    ExtrusionLine();
    ExtrusionLine(const Vec2f &a, const Vec2f &b, float len, const ExtrusionEntity *origin_entity);
    ExtrusionLine(const Vec2f &a, const Vec2f &b);

    bool is_external_perimeter() const;

    Vec2f                  a;
    Vec2f                  b;
    float                  len;
    const ExtrusionEntity *origin_entity;

    std::optional<SupportSpotsGenerator::SupportPointCause> support_point_generated = {};
    float form_quality            = 1.0f;
    float curled_up_height        = 0.0f;

    static const constexpr int Dim = 2;
    using Scalar                   = Vec2f::Scalar;
};

struct SliceConnection
{
    float area{};
    Vec3f centroid_accumulator              = Vec3f::Zero();
    Vec2f second_moment_of_area_accumulator = Vec2f::Zero();
    float second_moment_of_area_covariance_accumulator{};

    void add(const SliceConnection &other);

    void print_info(const std::string &tag) const;
};

Polygons get_brim(const ExPolygon& slice_polygon, const BrimType brim_type, const float brim_width);

class ObjectPart
{
public:
    float volume{};
    Vec3f volume_centroid_accumulator = Vec3f::Zero();
    float sticking_area{};
    Vec3f sticking_centroid_accumulator              = Vec3f::Zero();
    Vec2f sticking_second_moment_of_area_accumulator = Vec2f::Zero();
    float sticking_second_moment_of_area_covariance_accumulator{};
    bool  connected_to_bed = false;

    ObjectPart(
        const std::vector<const ExtrusionEntityCollection*>& extrusion_collections,
        const bool connected_to_bed,
        const coordf_t print_head_z,
        const coordf_t layer_height,
        const std::optional<Polygons>& brim
    );

    void add(const ObjectPart &other);

    void add_support_point(const Vec3f &position, float sticking_area);


    float compute_elastic_section_modulus(
        const Vec2f &line_dir,
        const Vec3f &extreme_point,
        const Integrals& integrals
    ) const;

    std::tuple<float, SupportPointCause> is_stable_while_extruding(const SliceConnection &connection,
                                    const ExtrusionLine   &extruded_line,
                                    const Vec3f           &extreme_point,
                                    float                  layer_z,
                                    const Params          &params) const;
};

using PartialObjects = std::vector<PartialObject>;

// Both support points and partial objects are sorted from the lowest z to the highest
std::tuple<SupportPoints, PartialObjects> full_search(const PrintObject *po, const PrintTryCancel& cancel_func, const Params &params);

void estimate_supports_malformations(std::vector<SupportLayer *> &layers, float supports_flow_width, const Params &params);
void estimate_malformations(std::vector<Layer *> &layers, const Params &params);


// NOTE: the boolean marks if the issue is critical or not for now.
std::vector<std::pair<SupportPointCause, bool>> gather_issues(const SupportPoints &support_points,
                                                             PartialObjects      &partial_objects);

}} // namespace Slic3r::SupportSpotsGenerator

#endif /* SRC_LIBSLIC3R_SUPPORTABLEISSUESSEARCH_HPP_ */
