/****************************************************************************
 *   Copyright (c) 2017 Zheng Lei (realthunder) <realthunder.dev@gmail.com> *
 *                                                                          *
 *   This file is part of the FreeCAD CAx development system.               *
 *                                                                          *
 *   This library is free software; you can redistribute it and/or          *
 *   modify it under the terms of the GNU Library General Public            *
 *   License as published by the Free Software Foundation; either           *
 *   version 2 of the License, or (at your option) any later version.       *
 *                                                                          *
 *   This library  is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of         *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the          *
 *   GNU Library General Public License for more details.                   *
 *                                                                          *
 *   You should have received a copy of the GNU Library General Public      *
 *   License along with this library; see the file COPYING.LIB. If not,     *
 *   write to the Free Software Foundation, Inc., 59 Temple Place,          *
 *   Suite 330, Boston, MA  02111-1307, USA                                 *
 *                                                                          *
 ****************************************************************************/

#ifndef PATH_AREA_H
#define PATH_AREA_H

#include <QCoreApplication>
#include <chrono>
#include <memory>
#include <vector>
#include <list>
#include <TopoDS.hxx>
#include <gp_Pln.hxx>
#include <gp_Circ.hxx>
#include <gp_GTrsf.hxx>

#include <Base/Console.h>
#include <Mod/Part/App/TopoShape.h>
#include <Mod/Part/App/PartPyCXX.h>
#include "Path.h"
#include "AreaParams.h"

class CArea;
class CCurve;
class Bnd_Box;

namespace Path
{

/** Store libarea algorithm configuration */
struct PathExport CAreaParams {
    PARAM_DECLARE(PARAM_FNAME,AREA_PARAMS_CAREA)
    CAreaParams();
};

/** Store all Area configurations */
struct PathExport AreaParams: CAreaParams {

    PARAM_DECLARE(PARAM_FNAME,AREA_PARAMS_AREA)

    bool operator==(const AreaParams &other) const {
#define AREA_COMPARE(_param) \
         if(PARAM_FIELD(NAME,_param)!=other.PARAM_FIELD(NAME,_param)) return false;
        PARAM_FOREACH(AREA_COMPARE,AREA_PARAMS_CAREA)
        PARAM_FOREACH(AREA_COMPARE,AREA_PARAMS_AREA)
        return true;
    }
    bool operator!=(const AreaParams &other) const {
        return !(*this == other);
    }

    void dump(const char *) const;

    AreaParams();
};

struct PathExport AreaStaticParams: AreaParams {
    AreaStaticParams();
};

/** libarea configurator
 *
 * It is kind of troublesome with the fact that libarea uses static variables to
 * config its algorithm. CAreaConfig makes it easy to safely customize libarea.
 */
struct PathExport CAreaConfig {

    /** For saving current libarea settings */
    PARAM_DECLARE(PARAM_FNAME,AREA_PARAMS_CAREA)

    /** The constructor automatically saves current setting and apply user defined ones
     *
     * \arg \c p user defined configurations
     * \arg \c noFitArgs if true, will override and disable arc fitting. Because
     * arc unfiting and fitting is lossy. And repeatedly perform these operation
     * may cause shape deformation. So it is best to delay arc fitting until the
     * final step*/
    CAreaConfig(const CAreaParams &p, bool noFitArcs=true);

    /** The destructor restores the setting, and thus exception safe.  */
    ~CAreaConfig();
};


/** Base class for FreeCAD wrapping of libarea */
class PathExport Area: public Base::BaseClass {

    TYPESYSTEM_HEADER();

public:
    struct Shape {
        short op;
        TopoDS_Shape shape;

        Shape(short opCode, const TopoDS_Shape &s)
            :op(opCode)
            ,shape(s)
        {}
    };

protected:
    std::list<Shape> myShapes;
    std::unique_ptr<CArea> myArea;
    std::unique_ptr<CArea> myAreaOpen;
    gp_Trsf myTrsf;
    AreaParams myParams;
    TopoDS_Shape myShapePlane;
    TopoDS_Shape myWorkPlane;
    TopoDS_Shape myShape;
    std::vector<std::shared_ptr<Area> > mySections;
    bool myHaveFace;
    bool myHaveSolid;
    bool myShapeDone;
    bool myProjecting;
    mutable int mySkippedShapes;

    static bool s_aborting;
    static AreaStaticParams s_params;

    /** Called internally to combine children shapes for further processing */
    void build();

    /** Called by build() to add children shape
     *
     * Mainly for checking if there is any faces for auto fill*/
    void addToBuild(CArea &area, const TopoDS_Shape &shape);

    /** Called internally to obtain the combined children shapes */
    TopoDS_Shape toShape(CArea &area, short fill, int reorient=0);

    /** Obtain a list of offset areas
     *
     * See #AREA_PARAMS_OFFSET for description of the arguments.
     */
    void makeOffset(std::list<std::shared_ptr<CArea> > &areas,
                    PARAM_ARGS_DEF(PARAM_FARG,AREA_PARAMS_OFFSET), bool from_center=false);

    /** Make a pocket of the combined shape
     *
     * User #AREA_PARAMS_POCKET setting in myParams.
     */
    TopoDS_Shape makePocket();

    void explode(const TopoDS_Shape &shape);

    TopoDS_Shape findPlane(const TopoDS_Shape &shape, gp_Trsf &trsf);

    std::list<Shape> getProjectedShapes(const gp_Trsf &trsf, bool inverse=true) const;

public:
    /** Declare all parameters defined in #AREA_PARAMS_ALL as member variable */
    PARAM_ENUM_DECLARE(AREA_PARAMS_ALL)

    Area(const AreaParams *params = nullptr);
    Area(const Area &other, bool deep_copy=true);
    virtual ~Area();

    bool isBuilt() const;

    /** Set a working plane
     *
     * \arg \c shape: a shape defining a working plane.
     *
     * The supplied shape does not need to be planar. Area will try to find planar
     * sub-shape (face, wire or edge). If more than one planar sub-shape is found,
     * it will prefer the top plane parallel to XY0 plane.
     *
     * If no working plane are set, Area will try to find a working plane from
     * the added children shape using the same algorithm
     */
    void setPlane(const TopoDS_Shape &shape);

    /** Return the current active workplane
     *
     * \arg \c trsf: optional return of a transformation matrix that will bring the
     * found plane to XY0 plane.
     *
     * If no workplane is set using setPlane(), the active workplane is derived from
     * the added children shapes using the same algorithm empolyed by setPlane().
     */
    TopoDS_Shape getPlane(gp_Trsf *trsf=nullptr);

    /** Add a child shape with given operation code
     *
     * No validation is done at this point. Exception will be thrown when asking
     * for output shape, if any of the children shapes is not valid or not
     * coplanar
     *
     * \arg \c shape: the child shape
     * \arg \c op: operation code, see #AREA_PARAMS_OPCODE
     */
    void add(const TopoDS_Shape &shape,PARAM_ARGS_DEF(PARAM_FARG,AREA_PARAMS_OPCODE));


    /** Generate an offset of the combined shape
     *
     * See #AREA_PARAMS_OFFSET for description of the arguments.
     * If more than one offset is requested, a compound shape is return
     * containing all offset shapes as wires regardless of \c Fill setting.
     */
    TopoDS_Shape makeOffset(int index=-1, PARAM_ARGS_DEF(PARAM_FARG,AREA_PARAMS_OFFSET),
            int reoirent=0, bool from_center=false);

    /** Make a pocket of the combined shape
     *
     * See #AREA_PARAMS_POCKET for description of the arguments.
     */
    TopoDS_Shape makePocket(int index=-1, PARAM_ARGS_DEF(PARAM_FARG,AREA_PARAMS_POCKET));

    /** Make a pocket of the combined shape
     *
     * \arg \c heights: optional customized heights of each section. The
     * meaning of each height depends on section mode. If none is given,
     * the section heights is determined by the section settings in this
     * Area object (configured through setParams()).
     * \arg \c plane: the section plane if the section mode is
     * SectionModeWorkplane, otherwise ignored
     *
     * See #AREA_PARAMS_EXTRA for description of the arguments. Currently, there
     * is only one argument, namely \c mode for section mode.
     */
    std::vector<std::shared_ptr<Area> > makeSections(
            PARAM_ARGS_DEF(PARAM_FARG,AREA_PARAMS_SECTION_EXTRA),
            const std::vector<double> &heights = std::vector<double>(),
            const TopoDS_Shape &plane = TopoDS_Shape());

    /** Config this Area object */
    void setParams(const AreaParams &params);


    const std::list<Shape> getChildren() const {
        return myShapes;
    }

    /** Get the current configuration */
    const AreaParams &getParams() const {
        return myParams;
    }

    /** Clean internal caches
     *
     * The combined shapes is cached internally to make other operation more
     * efficient, such as makeOffset() and makePocket()
     *
     * \arg \c deleteShapes: if true, delete all children shapes.
     */
    void clean(bool deleteShapes=false);

    /** Get the combined shape
     * \arg \c index: index of the section, -1 for all sections. No effect on
     * non-sectioned area.
     */
    TopoDS_Shape getShape(int index=-1);

    /** Return the number of sections */
    std::size_t getSectionCount() {
        build();
        return mySections.size();
    }

    /** Add a OCC wire shape to CArea
     *
     * \arg \c area: output converted curved object to here
     * \arg \c wire: input wire object
     * \arg \c trsf: optional transform matrix to transform the wire shape into
     * XY0 plane.
     * \arg \c deflection: for discretizing non circular curves
     * \arg \c to_edges: if true, discretize all curves, and insert as open
     * line segments
     * */
    static void addWire(CArea &area, const TopoDS_Wire &wire, const gp_Trsf *trsf=nullptr,
            double deflection=0.01, bool to_edges=false);

    /** Add a OCC generic shape to CArea
     *
     * \arg \c area: output converted curved object to here
     * \arg \c shape: input shape object
     * \arg \c trsf: optional transform matrix to transform the wire shape into
     * XY0 plane.
     * \arg \c deflection: for defecting non circular curves
     * \arg \c plane: a shape for testing coplanar
     * \arg \c force_coplaner: if true, discard non-coplanar shapes.
     * \arg \c areaOpen: for collecting open curves. If not supplied, open
     * curves are added to \c area
     * \arg \c to_edges: separate open wires to individual edges
     * \arg \c reorient: reorient closed wires for wire only shape
     *
     * \return Returns the number of non coplaner. Planar testing only happens
     * if \c plane is supplied
     * */
    static int addShape(CArea &area, const TopoDS_Shape &shape, const gp_Trsf *trsf=nullptr,
            double deflection=0.01,const TopoDS_Shape *plane = nullptr,
            bool force_coplanar=true, CArea *areaOpen=nullptr, bool to_edges=false,
            bool reorient=true);

    /** Convert curves in CArea into an OCC shape
     *
     * \arg \c area: input area object
     * \arg \c fill: if true, create a face object from the wires
     * \arg \c trsf: optional transform matrix to transform the shape back into
     * its original position.
     * */
    static TopoDS_Shape toShape(const CArea &area, bool fill,
            const gp_Trsf *trsf=nullptr, int reoirent=0);

    /** Convert a single curve into an OCC wire
     *
     * \arg \c curve: input curve object
     * \arg \c trsf: optional transform matrix to transform the shape back into
     * its original position.
     * */
    static TopoDS_Shape toShape(const CCurve &curve, const gp_Trsf *trsf=nullptr, int reorient=0);

    /** Check if two OCC shape is coplanar */
    static bool isCoplanar(const TopoDS_Shape &s1, const TopoDS_Shape &s2);

    /** Group shapes by their plane, and return a list of sorted wires
     *
     * The output wires is ordered by its occupied plane, and sorted to
     * minimize traval distance
     *
     * \arg \c shapes: input list of shapes.
     * \arg \c has_start: if false or pstart is 0, then a start point will be
     * auto selected.
     * \arg \c pstart: optional start point. If has_start is false, then the
     * auto selected start point will be returned with this point if not NULL.
     * \arg \c pend: optional output containing the ending point of the returned
     * \arg \c stepdown_hint: optional output of a hint of step down as the max
     * distance between two sections.
     * \arg \c arc_plane: optional arc plane selection, if given the found plane
     * will be returned. See #AREA_PARAMS_ARC_PLANE for more details.
     *
     * See #AREA_PARAMS_SORT for other arguments
     *
     * \return sorted wires
     */
    static std::list<TopoDS_Shape> sortWires(const std::list<TopoDS_Shape> &shapes,
            bool has_start=false, gp_Pnt *pstart=nullptr, gp_Pnt *pend=nullptr, double *stepdown_hint=nullptr,
            short *arc_plane = nullptr, PARAM_ARGS_DEF(PARAM_FARG,AREA_PARAMS_SORT));

    /** Convert a list of wires to gcode
     *
     * \arg \c path: output toolpath
     * \arg \c shapes: input list of shapes
     * \arg \c pstart: output start point,
     * \arg \c pend: optional output containing the ending point of the returned
     *
     * See #AREA_PARAMS_PATH for other arguments
     */
    static void toPath(Toolpath &path, const std::list<TopoDS_Shape> &shapes,
            const gp_Pnt *pstart=nullptr, gp_Pnt *pend=nullptr,
            PARAM_ARGS_DEF(PARAM_FARG,AREA_PARAMS_PATH));

    static int project(TopoDS_Shape &out, const TopoDS_Shape &in,
                       const AreaParams *params = nullptr,
                       const TopoDS_Shape *work_plane = nullptr);

    static void setWireOrientation(TopoDS_Wire& wire, const gp_Dir &dir, bool ccw);

    PARAM_ENUM_DECLARE(AREA_PARAMS_PATH)

    static void abort(bool aborting);
    static bool aborting();

    static void setDefaultParams(const AreaStaticParams &params);
    static const AreaStaticParams &getDefaultParams();

    static void showShape(const TopoDS_Shape &shape, const char *name, const char *fmt=nullptr, ...);
};

} //namespace Path

#endif //PATH_AREA_H
