/*
 * LibrePCB - Professional EDA for everyone!
 * Copyright (C) 2013 LibrePCB Developers, see AUTHORS.md for contributors.
 * https://librepcb.org/
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#ifndef LIBREPCB_CORE_PATH_H
#define LIBREPCB_CORE_PATH_H

/*******************************************************************************
 *  Includes
 ******************************************************************************/
#include "../exceptions.h"
#include "vertex.h"

#include <type_safe/constrained_type.hpp>

#include <QtCore>
#include <QtGui>

#include <optional>

/*******************************************************************************
 *  Namespace / Forward Declarations
 ******************************************************************************/
namespace librepcb {

/*******************************************************************************
 *  Class Path
 ******************************************************************************/

/**
 * @brief The Path class represents a list of vertices connected by straight
 * lines or circular arc segments
 *
 * This class is similar to a polygon or polyline, but it doesn't have
 * properties like width or layer. It's only a list of coordinates which are
 * connected together by straight lines or circular arc segments.
 *
 * A path may be closed (first point == last point) or open (first point != last
 * point).
 *
 * For a valid path, minimum two vertices are required. Paths with less than two
 * vertices are useless and thus considered as invalid.
 */
class Path final {
  Q_DECLARE_TR_FUNCTIONS(Path)

public:
  // Constructors / Destructor
  Path() noexcept : mVertices(), mPainterPathPx() {}
  Path(const Path& other) noexcept;
  explicit Path(const QVector<Vertex>& vertices) noexcept
    : mVertices(vertices) {}
  explicit Path(const SExpression& node);
  ~Path() noexcept {}

  // Getters
  bool isClosed() const noexcept;
  bool isCurved() const noexcept;
  bool isZeroLength() const noexcept;
  bool isOnGrid(const PositiveLength& gridInterval) const noexcept;
  QVector<Vertex>& getVertices() noexcept {
    invalidatePainterPath();
    return mVertices;
  }
  const QVector<Vertex>& getVertices() const noexcept { return mVertices; }
  UnsignedLength getTotalStraightLength() const noexcept;
  qreal calcAreaOfStraightSegments() const noexcept;
  Point calcNearestPointBetweenVertices(const Point& p) const noexcept;
  Path cleaned() const noexcept;
  Path toClosedPath() const noexcept;
  Path toOpenPath() const noexcept;
  QVector<Path> toOutlineStrokes(const PositiveLength& width) const noexcept;
  const QPainterPath& toQPainterPathPx() const noexcept;
  QString toSvgPathMm() const noexcept;

  // Transformations
  Path& translate(const Point& offset) noexcept;
  Path translated(const Point& offset) const noexcept;
  Path& mapToGrid(const PositiveLength& gridInterval) noexcept;
  Path mappedToGrid(const PositiveLength& gridInterval) const noexcept;
  Path& rotate(const Angle& angle, const Point& center = Point(0, 0)) noexcept;
  Path rotated(const Angle& angle,
               const Point& center = Point(0, 0)) const noexcept;
  Path& mirror(Qt::Orientation orientation,
               const Point& center = Point(0, 0)) noexcept;
  Path mirrored(Qt::Orientation orientation,
                const Point& center = Point(0, 0)) const noexcept;
  Path& reverse() noexcept;
  Path reversed() const noexcept;
  Path& flattenArcs(const PositiveLength& maxTolerance) noexcept;
  Path flattenedArcs(const PositiveLength& maxTolerance) const noexcept;

  // General Methods
  void addVertex(const Vertex& vertex) noexcept;
  void addVertex(const Point& pos, const Angle& angle = Angle::deg0()) noexcept;
  void insertVertex(int index, const Vertex& vertex) noexcept;
  void insertVertex(int index, const Point& pos,
                    const Angle& angle = Angle::deg0()) noexcept;
  bool clean() noexcept;
  bool close() noexcept;
  bool open() noexcept;

  /**
   * @brief Serialize into ::librepcb::SExpression node
   *
   * @param root    Root node to serialize into.
   */
  void serialize(SExpression& root) const;

  // Operator Overloadings
  Path& operator=(const Path& rhs) noexcept;
  bool operator==(const Path& rhs) const noexcept {
    return mVertices == rhs.mVertices;
  }
  bool operator!=(const Path& rhs) const noexcept { return !(*this == rhs); }

  /**
   * @brief The "<" operator to compare two ::librepcb::Path objects
   *
   * Useful for sorting path lists/sets (e.g. to for canonical order in
   * files), or to store them in a QMap.
   *
   * @param rhs The right hand side object.
   *
   * @return true if this path is smaller, else false
   */
  bool operator<(const Path& rhs) const noexcept;

  // Static Methods
  static Path line(const Point& p1, const Point& p2,
                   const Angle& angle = Angle::deg0()) noexcept;
  static Path circle(const PositiveLength& diameter) noexcept;
  static Path donut(const PositiveLength& outerDiameter,
                    const PositiveLength& innerDiameter) noexcept;
  static Path obround(const PositiveLength& width,
                      const PositiveLength& height) noexcept;
  static Path obround(const Point& p1, const Point& p2,
                      const PositiveLength& width) noexcept;
  static Path arcObround(const Point& p1, const Point& p2, const Angle& angle,
                         const PositiveLength& width) noexcept;
  static std::optional<Path> flatCapLine(const Point& p1, const Point& p2,
                                         const Angle& angle,
                                         const PositiveLength& width) noexcept;
  static Path rect(const Point& p1, const Point& p2) noexcept;
  static Path centeredRect(
      const PositiveLength& width, const PositiveLength& height,
      const UnsignedLength& cornerRadius = UnsignedLength(0)) noexcept;
  static Path chamferedRect(const PositiveLength& width,
                            const PositiveLength& height,
                            const UnsignedLength& chamferSize, bool topLeft,
                            bool topRight, bool bottomLeft,
                            bool bottomRight) noexcept;
  static Path trapezoid(const PositiveLength& width,
                        const PositiveLength& height, const Length& dw,
                        const Length dh) noexcept;
  static Path octagon(
      const PositiveLength& width, const PositiveLength& height,
      const UnsignedLength& cornerRadius = UnsignedLength(0)) noexcept;
  static Path flatArc(const Point& p1, const Point& p2, const Angle& angle,
                      const PositiveLength& maxTolerance) noexcept;

  /**
   * @brief Convert multiple ::librepcb::Path objects to a QPainterPath
   *
   * The paths are united, so you get the union of all the passed paths.
   *
   * @param paths   The paths to convert.
   * @param area    Whether the passed paths should be interpreted as areas
   *                (true) or strokes (false).
   * @return        The QPainterPath with the united paths.
   */
  static QPainterPath toQPainterPathPx(const QVector<Path>& paths,
                                       bool area) noexcept;

private:  // Methods
  void invalidatePainterPath() const noexcept {
    mPainterPathPx = QPainterPath();
  }

private:  // Data
  QVector<Vertex> mVertices;
  mutable QPainterPath mPainterPathPx;  // cached path for #toQPainterPathPx()
};

/*******************************************************************************
 *  Non-Member Functions
 ******************************************************************************/

inline std::size_t qHash(const Path& key, std::size_t seed = 0) noexcept {
  return qHashRange(key.getVertices().begin(), key.getVertices().end(), seed);
}

/*******************************************************************************
 *  Class NonEmptyPath
 ******************************************************************************/

struct NonEmptyPathVerifier {
  template <typename Value, typename Predicate>
  static constexpr auto verify(Value&& val, const Predicate& p) ->
      typename std::decay<Value>::type {
    return p(val)
        ? std::forward<Value>(val)
        : (throw RuntimeError(__FILE__, __LINE__,
                              Path::tr("Path doesn't contain vertices!")),
           std::forward<Value>(val));
  }
};

struct NonEmptyPathConstraint {
  bool operator()(const Path& p) const noexcept {
    return p.getVertices().count() > 0;
  }
};

/**
 * NonEmptyPath is a wrapper around a librepcb::Path object which is
 * guaranteed to always contain at least one vertex.
 *
 * The constructor throws an exception if constructed from a librepcb::Path
 * without vertices.
 */
using NonEmptyPath = type_safe::constrained_type<Path, NonEmptyPathConstraint,
                                                 NonEmptyPathVerifier>;

inline std::size_t qHash(const NonEmptyPath& key,
                         std::size_t seed = 0) noexcept {
  return ::qHash(*key, seed);
}

inline NonEmptyPath makeNonEmptyPath(const Point& pos) noexcept {
  return NonEmptyPath(Path({Vertex(pos)}));
}

/*******************************************************************************
 *  Class StraightAreaPath
 ******************************************************************************/

struct StraightAreaPathVerifier {
  template <typename Value, typename Predicate>
  static constexpr auto verify(Value&& val, const Predicate& p) ->
      typename std::decay<Value>::type {
    return p(val) ? std::forward<Value>(val)
                  : (throw RuntimeError(
                         __FILE__, __LINE__,
                         Path::tr("Path is not fillable or contains arcs!")),
                     std::forward<Value>(val));
  }
};

struct StraightAreaPathConstraint {
  bool operator()(const Path& p) const noexcept {
    return (p.getVertices().count() >= 4) && p.isClosed() && (!p.isCurved());
  }
};

/**
 * StraightAreaPath is a wrapper around a ::librepcb::Path object which is
 * guaranteed to always contain a closed path with at least 4 vertices and no
 * arcs (only straight segments).
 *
 * The constructor throws an exception if constructed from a ::librepcb::Path
 * which does not fulfil these requirements.
 */
using StraightAreaPath =
    type_safe::constrained_type<Path, StraightAreaPathConstraint,
                                StraightAreaPathVerifier>;

inline std::size_t qHash(const StraightAreaPath& key,
                         std::size_t seed = 0) noexcept {
  return ::qHash(*key, seed);
}

/*******************************************************************************
 *  End of File
 ******************************************************************************/

}  // namespace librepcb

Q_DECLARE_METATYPE(librepcb::Path)

#endif
