//
//  GSPath.h
//  FontBezier Doc
//
//  Created by Georg Seifert on 19.10.05.
//  Copyright 2005 schriftgestaltung.de. All rights reserved.
//

#import <GlyphsCore/GSContainerProtocol.h>
#import <GlyphsCore/GSNode.h>
#import <GlyphsCore/GSShape.h>
#import <GlyphsCore/GSDrawPath.h>
#import <GlyphsCore/GSDrawSlice.h>

NS_ASSUME_NONNULL_BEGIN

@class GSGlyph;
@class GSLayer;
@class GSElement;
@class GSHint;
@class GSPathSegment;
#ifndef GLYPHS_LITE
@class GSPathHash;
#endif
@protocol GSPenProtocol;

/// The traversal direction of a path.
typedef NS_CLOSED_ENUM(int8_t, GSPathDirection) {
	GSCounterClockWise NS_SWIFT_NAME(counterClockwise) = -1,
	GSClockWise NS_SWIFT_NAME(clockwise) = 1,
} NS_SWIFT_NAME(GSPath.Direction);

/// The default tolerance for converting cubic curve segments to quadratic curve segments.
extern CGFloat GSPathCurveError;

/// A graphical path made of nodes.
///
/// A path describes one continuous outline contour; it is not divided into separate subpaths.
///
/// The nodes are stored in drawing order.
/// On-curve nodes lie on the contour and define segment endpoints.
/// Off-curve nodes control the shape of curve segments.
///
/// A segment runs from one on-curve node to the next on-curve node.
/// The on-curve node type describes the segment ending at that node.
/// A *path time* identifies a position within a segment by combining the ending on-curve node integer index with a fractional segment position.
///
/// Closed paths connect the final node to the first node.
/// Open paths do not wrap at their ends.
/// Navigation path APIs wrap indices for closed paths and reject out-of-range indices for open paths.
///
/// A path is expected to contain nodes during normal use.
/// Empty paths can exist immediately after creation, but many path operations assume a populated path.
/// Paths without on-curve nodes are interpreted as using quadratic path segments with implicit on-curve nodes.

@interface GSPath : GSShape <GSContainerProtocol, GSDrawPath> {
	NSMutableArray<GSNode *> *_nodes;
	BOOL _closed;

#pragma GSAutoCodeStart ivars

#pragma GSAutoCodeEnd ivars
}

/// Whether the final node connects to the first node.
///
/// Setting this property does not adjust the nodes.
/// Use ``setClosePath:`` to adjust endpoint nodes when opening or closing a path.
@property (nonatomic) BOOL closed NS_SWIFT_NAME(isClosed);

/// Creates a path from a Glyphs file dictionary representation.
///
/// Returns `nil` when the representation contains no nodes or invalid data.
///
/// - Parameter formatVersion: The Glyphs file format version used to interpret the representation.
- (nullable instancetype)initWithDict:(NSDictionary<NSString *, id> *)pathDict format:(GSFormatVersion)formatVersion;

/// Creates a path from a sequence of node element strings.
///
/// Separate node element strings with semicolons.
/// Do not include surrounding quotation marks or trailing whitespace.
///
/// For example:
///
/// ```
/// @"369 29 o;450 61 o;509 118 o;542 196 o;542 241 qs;542 324 o;482 381 q;322 29 l"
/// ```
///
/// May return `nil` when the path string cannot be parsed.
///
/// - Parameter isClosed: Whether to create a closed path.
+ (nullable instancetype)pathWithString:(NSString *)pathString isClosed:(BOOL)isClosed;

/// Whether another path has the same closed state, node count, and node positions.
///
/// Node positions are compared with a small tolerance.
/// Node types, connections, and attributes are not compared.
- (BOOL)isEqualToPath:(GSPath *)otherPath;

#ifndef GLYPHS_VIEWER

/// Writes a Glyphs file representation.
///
/// Returns `NO` when the path uses data that cannot be represented in the requested file format.
///
/// - Parameter formatVersion: The Glyphs file format version to write.
/// - Parameter error: On failure, an error describing why the path could not be written.
- (BOOL)saveToFile:(FILE *)file format:(GSFormatVersion)formatVersion context:(nullable GSSaveContext *)context error:(NSError **)error;

/// Finds the point on the path nearest a point.
///
/// A path time identifies a position within a path segment.
/// Its integer part is the index of the ending on-curve node, and its fractional part is the position within the segment.
///
/// - Parameter pathTime: On return, the path time of the nearest point.
/// - Returns: The nearest point.
- (NSPoint)nearestPointOnPath:(NSPoint)loc pathTime:(out CGFloat *)pathTime NS_REFINED_FOR_SWIFT;

/// Returns the on-curve node nearest a path time.
- (nullable GSNode *)nearestNodeWithPathTime:(CGFloat)pathTime NS_SWIFT_NAME(nearestNode(atPathTime:));

/// Inserts an on-curve node at a path time while preserving the path shape.
///
/// The integer part of the path time identifies the ending on-curve node of a segment.
/// The fractional part identifies a position within the segment.
///
/// - Returns: The inserted node, the existing node at a segment boundary, or `nil` when the operation is unsupported.
- (nullable GSNode *)insertNodeWithPathTime:(CGFloat)pathTime NS_SWIFT_NAME(insertNode(atPathTime:));

#endif

/// The point at a path time.
///
/// The path time must identify a position on the path.
- (NSPoint)pointAtPathTime:(CGFloat)pathTime NS_REFINED_FOR_SWIFT;

/// @name Nodes

/// The ordered nodes describing the path.
///
/// A node can belong to only one path.
/// Remove a node from its current path before adding it to another path.
///
/// Setting this property replaces all nodes.
/// Use path mutation methods rather than modifying the returned array directly.
@property (strong, nonatomic) NSArray<GSNode *> *nodes;

/// Replaces all nodes without registering undo or notifying the layer.
- (void)setNodesFast:(NSArray<GSNode *> *)nodes;

/// The number of nodes.
- (NSUInteger)countOfNodes;

/// Appends a node and aligns its position to the layer grid.
- (void)addNode:(GSNode *)node;

/// Appends a new node and aligns its position to the layer grid.
- (void)addNodeWithPosition:(NSPoint)position type:(GSNodeType)type connection:(GSNodeConnection)connection attributes:(nullable NSDictionary<GSShapeKey, id> *)attributes;

/// Appends a new node without registering undo, aligning its position, or notifying the layer.
- (void)addNodeFastWithPosition:(NSPoint)position type:(GSNodeType)type connection:(GSNodeConnection)connection attributes:(nullable NSDictionary<GSShapeKey, id> *)attributes;

/// Appends a cubic curve segment and aligns its node positions to the layer grid.
///
/// - Parameter attributes: Attributes for the ending on-curve node.
- (void)addCurveWithOff1:(NSPoint)off1 off2:(NSPoint)off2 end:(NSPoint)end connection:(GSNodeConnection)connection attributes:(nullable NSDictionary<GSShapeKey, id> *)attributes;

/// Appends a cubic curve segment without registering undo, aligning its node positions, or notifying the layer.
///
/// - Parameter attributes: Attributes for the ending on-curve node.
- (void)addCurveFastWithOff1:(NSPoint)off1 off2:(NSPoint)off2 end:(NSPoint)end connection:(GSNodeConnection)connection attributes:(nullable NSDictionary<GSShapeKey, id> *)attributes;

/// Appends a node without registering undo, aligning its position, or notifying the layer.
- (void)addNodeFast:(GSNode *)node;

/// Appends nodes.
- (void)addNodes:(NSArray<GSNode *> *)nodes;

/// Removes nodes.
- (void)removeNodes:(NSArray<GSNode *> *)nodes;

/// Inserts a node at an index and aligns its position to the layer grid.
- (void)insertObject:(GSNode *)node inNodesAtIndex:(NSInteger)idx NS_SWIFT_NAME(insertNode(_:atIndex:));

/// Removes the node at an index.
- (void)removeObjectFromNodesAtIndex:(NSInteger)idx NS_SWIFT_NAME(removeNode(atIndex:));

/// Removes a node.
- (void)removeNode:(GSNode *)node;

/// Removes a node without registering undo, updating the layer selection, or notifying the layer.
- (void)removeNodeFast:(GSNode *)node;
#ifndef GLYPHS_VIEWER

/// Removes a node and adjusts adjacent segments to approximate the path shape.
///
/// - Returns: Whether the node could be removed.
- (BOOL)removeNodeCheckKeepShape:(GSNode *)node;

/// Removes a node and adjusts adjacent segments to approximate the path shape.
///
/// - Parameter normalizeHandles: Whether to normalize the remaining handles before fitting the adjusted curve.
/// - Parameter error: On failure, an error describing why the node could not be removed.
/// - Returns: Whether the node could be removed.
- (BOOL)removeNodeCheckKeepShape:(GSNode *)node normalizeHandles:(BOOL)normalizeHandles error:(NSError **)error;

/// Removes a node and adjusts adjacent nodes to keep the path structurally valid.
- (void)removeNodeCheck:(GSNode *)node;

/// Notifies the path that one of its elements changed.
///
/// This invalidates path caches and forwards the change to the layer.
- (void)elementDidChange:(nullable id)element;

#endif

/// The lowest on-curve node, preferring the leftmost node at equal height.
///
/// For a path without on-curve nodes, returns an off-curve node.
- (nullable GSNode *)findMinNode;

/// The node used to begin traversing a closed path, or `nil` for an open path.
- (nullable GSNode *)findStartNode;

/// Moves trailing off-curve nodes to the beginning of a closed path.
///
/// This ensures that the final node is on-curve.
- (void)fixStartNode;

/// Makes a node the start node of the path.
///
/// The start node of a closed path is the final node in ``nodes``.
/// For open paths, this method is only effective for the end node, reversing the path direction.
- (void)makeNodeFirst:(GSNode *)node;

/// Whether the path uses TrueType-compatible node types.
///
/// Paths without curve node types are TrueType-compatible.
- (BOOL)hasTrueTypeOutline;

/// Whether the path contains quadratic curve node types.
///
/// A path with off-curve nodes and no cubic or quartic curve node is treated as quadratic.
- (BOOL)hasTrueTypeCurve;

/// Whether the path contains curve node types that are not cubic.
///
/// A path with off-curve nodes and no cubic curve node is treated as non-cubic.
- (BOOL)hasNonCubicCurves;

/// Whether the path contains at least one on-curve node.
- (BOOL)hasOncurve;

#ifndef GLYPHS_LITE
/// Whether any node contains pen point data.
- (BOOL)hasPenPoints;
#endif

/// Stroke outlines built from the path stroke attributes.
///
/// Returns an empty array when the stroke cannot be expanded.
/// Axis-aligned cap styles use the layer italic angle when a layer is available.
- (NSArray<GSPath *> *)expandedStroke;

#ifndef GLYPHS_LITE
/// The index preceding the first on-curve node for TrueType path traversal.
- (NSUInteger)indexOfTTStartNode;
#endif
/// Opens or closes the path and adjusts its endpoint nodes.
- (void)setClosePath:(BOOL)closed;

/// Opens or closes the path and adjusts its endpoint nodes.
///
/// - Parameter fixStartNode: Whether to move the start node to the end when closing a path whose endpoints do not coincide.
- (void)setClosePath:(BOOL)closed fixStartNode:(BOOL)fixStartNode;

/// Moves an off-curve node by an offset while preserving its locked direction.
///
/// - Parameter point: The offset by which to move the node.
- (void)setLocked:(GSNode *)node withPoint:(NSPoint)point;

/// Aligns a node opposite a point about a center point while preserving its distance from the center point.
- (void)setSmooth:(GSNode *)node withCenterPoint:(NSPoint)centerPoint oppositePoint:(NSPoint)oppositePoint;

/// Aligns a node opposite another node about a center node while preserving its distance from the center node.
- (void)setSmooth:(GSNode *)node withCenterNode:(GSNode *)centerNode oppositeNode:(GSNode *)oppositeNode;

/// Moves an off-curve node by an offset, constrained using a corresponding node in another path.
///
/// The paths must have corresponding node structures.
///
/// - Parameter shadowNode: The corresponding node at the same index in the other path.
/// - Parameter aPoint: The offset by which to move the node.
- (void)setLocked:(GSNode *)node shadow:(GSNode *)shadowNode withPoint:(NSPoint)aPoint;

/// Moves an off-curve node to the reflection of a point about a center point.
- (void)setMirrored:(GSNode *)node withCenterPoint:(NSPoint)centerPoint oppositePoint:(NSPoint)oppositePoint;

/// Normalizes node types and connections based on adjacent nodes.
- (void)checkConnections;

/// Sets a node connection based on the alignment of adjacent nodes.
- (void)checkConnectionForNode:(GSNode *)node;

/// The node at an index.
///
/// For a closed path, indices wrap around the path.
/// For an open path, returns `nil` when the index is out of bounds.
- (nullable GSNode *)nodeAtIndex:(NSInteger)idx;

/// The index of a node, or `NSNotFound` when the node is not in the path.
- (NSUInteger)indexOfNode:(GSNode *)node NS_REFINED_FOR_SWIFT;

/// For open paths, returns the first node of the path, otherwise `nil`.
@property (nonatomic, readonly, nullable) GSNode *startNode;

/// For open paths, returns the last node of the path, otherwise `nil`.
@property (nonatomic, readonly, nullable) GSNode *endNode;

#ifndef GLYPHS_VIEWER
/// The direction in which the nodes traverse the path.
- (GSPathDirection)direction;

/// Whether the path encloses another path.
- (BOOL)enclosesPath:(GSPath *)other NS_SWIFT_NAME(encloses(_:));
#endif

/// Reverses the path direction.
- (void)reverse;

/// Reverses the path direction without registering undo or notifying the layer.
- (void)reverseFast;
#ifndef GLYPHS_VIEWER
/// Removes redundant nodes and rounds node positions to the layer grid.
///
/// Cleanup removes nodes that do not affect the path shape within the cleanup tolerance.
/// It also closes an open path when its endpoints are nearly coincident.
- (void)cleanUp;

/// Removes redundant nodes and rounds node positions to a grid.
///
/// Cleanup removes nodes that do not affect the path shape within the cleanup tolerance.
/// Node positions are rounded when the grid is meaningful.
/// It also closes an open path when its endpoints are nearly coincident.
- (void)cleanUpGrid:(GSGridSettings)grid;

/// Adds the node indices that cleanup would keep.
///
/// Existing indices in the index set are preserved.
///
/// - Parameter requiredNodeIndexes: The index set to receive the required node indices.
- (void)findRequiredNodeIndexes:(NSMutableIndexSet *)requiredNodeIndexes grid:(GSGridSettings)grid NS_REFINED_FOR_SWIFT;

/// Removes adjacent on-curve nodes at the same position.
- (void)removeDuplicateNodes;

/// Converts the path to cubic curve segments.
- (void)convertToCubic;

#ifndef GLYPHS_LITE

/// The tolerance used when converting the path to quadratic curve segments.
///
/// The font’s `TrueType Curve Error` custom parameter overrides ``GSPathCurveError`` when it is present and positive.
- (CGFloat)convertToTrueTypeError;

/// Converts the path to quadratic curve segments.
///
/// The conversion uses ``convertToTrueTypeError`` as its curve tolerance.
- (void)convertToQuadratic NS_REFINED_FOR_SWIFT;

/// Converts the path to quadratic curve segments.
///
/// The conversion uses ``convertToTrueTypeError`` as its curve tolerance.
- (BOOL)convertToQuadraticWithError:(NSError **)error NS_SWIFT_NAME(convertToQuadratic());

/// Converts special curve node types to Bezier curve segments.
- (void)convertToBezier;

/// Applies converted quadratic segments to existing nodes.
///
/// The segments and nodes must describe the same path segments in the same order.
///
/// - Returns: Whether the converted segments were applied.
+ (BOOL)applySolutions:(NSArray<GSPathSegment *> *)segments toNodes:(NSArray<GSNode *> *)nodes grid:(GSGridSettings)grid;

#endif
#endif

/// A path tracing the same outline using primitive node types and connections.
///
/// - Node types are limited to `GSNodeTypeLine`, `GSNodeTypeCubicCurve`, `GSNodeTypeQuadraticCurve`, and `GSNodeTypeOffCurve`.
/// - Connections are limited to `GSNodeConnectionSharp` and `GSNodeConnectionSmooth`.
/// - Path components (corners, caps, segments, brushes) are decomposed.
///
/// Returns `self` when the path is already primitive.
- (GSPath *)primitiveRepresentation;

/// Paths tracing the same outline using primitive node types and connections.
///
/// The secondary pen receives secondary outline information while path components are decomposed.
///
/// - Parameter secondPen: A pen for secondary outline information.
- (NSArray <GSPath *> *)primitiveRepresentationSecondaryPen:(nullable NSObject<GSPenProtocol> *)secondPen NS_SWIFT_NAME(primitiveRepresentation(secondaryPen:));

/// Whether the path already uses primitive node types and connections.
@property (nonatomic, readonly) BOOL isPrimitive;

#ifndef GLYPHS_VIEWER
#ifndef GLYPHS_LITE
/// Converts the path to Hobby curve segments.
- (void)convertToHobby;
#endif
#endif

/// The bounds of the path.
///
/// For curve segments, the bounds enclose the curve and may exclude control points outside the curve.
///
/// Returns `CGRectNull` when the path has no drawable bounds.
- (NSRect)bounds;

/// The bounds of the path nodes.
///
/// For curve segments, the bounds include off-curve control points and can be larger than ``bounds``.
///
/// Returns `CGRectNull` when the path has no nodes.
- (NSRect)fastBounds;

/// The bounds of the path nodes after applying a transform.
///
/// For curve segments, the bounds include off-curve control points and can be larger than ``bounds``.
///
/// Returns `CGRectNull` when the path has no nodes.
- (NSRect)fastBoundsTransform:(nullable NSAffineTransform *)transform;

#ifndef GLYPHS_VIEWER
#ifndef GLYPHS_LITE
/// The unsigned area enclosed by the path.
///
/// Open paths are measured as if their endpoints were connected.
- (CGFloat)area;

/// The length of all path segments.
- (CGFloat)length;
#endif

/// A path with path components expanded into outline nodes.
///
/// Stroke, corner, cap, segment, and brush attributes are resolved into the returned path.
- (GSPath *)flattenedPath;

/// Adds nodes at extrema of cubic curve segments.
///
/// A nearby node may be moved to the extremum instead.
///
/// - Parameter force: Whether to insert extrema when doing so may introduce a bump.
/// - Parameter checkSelection: Whether to restrict the operation to segments containing a selected node when the layer selection is not empty.
- (void)addExtremes:(BOOL)force checkSelection:(BOOL)checkSelection NS_SWIFT_NAME(addExtremes(force:checkSelection:));
#ifndef GLYPHS_LITE
/// Adds nodes at inflection points of cubic curve segments.
- (void)addInflections;
#endif
#endif

#ifndef GLYPHS_LITE
/// The tangent angle at a node.
///
/// Nonnegative direction values use following nodes.
/// Negative direction values use preceding nodes.
///
/// Returns `NSNotFound` when no tangent can be determined.
- (CGFloat)tangentAngleAtNode:(GSNode *)node direction:(int)direction NS_SWIFT_NAME(__tangentAngle(at:direction:));
#endif

/// The tangent angle at a node index.
///
/// Nonnegative direction values use following nodes.
/// Negative direction values use preceding nodes.
///
/// Returns `NSNotFound` when no tangent can be determined.
- (CGFloat)tangentAngleAtNodeAtIndex:(NSInteger)idx direction:(int)direction NS_SWIFT_NAME(__tangentAngle(atNodeIndex:direction:));

/// The tangent unit vector at a node index.
///
/// For the final node of an open path, the vector uses preceding nodes.
/// Otherwise, the vector uses following nodes.
///
/// Returns a point whose x value is `NSNotFound` when no tangent can be determined.
- (NSPoint)unitVectorAtNodeAtIndex:(NSInteger)nodeIdx NS_SWIFT_NAME(__unitVector(atNodeIndex:));

/// The tangent unit vector at a node index.
///
/// Nonnegative direction values use following nodes.
/// Negative direction values use preceding nodes.
///
/// Returns a point whose x value is `NSNotFound` when no tangent can be determined.
- (NSPoint)unitVectorAtNodeAtIndex:(NSInteger)nodeIdx direction:(int)direction NS_SWIFT_NAME(__unitVector(atNodeIndex:direction:));

/// An AppKit drawing path for the path.
///
/// For drawing a complete layer, prefer the layer drawing path when available.
///
/// Returns `nil` when the path has no nodes.
- (nullable NSBezierPath *)bezierPath;

/// The path represented as path segments.
///
/// A segment runs from one on-curve node to the next on-curve node.
/// Off-curve nodes between them become control points of the segment.
///
/// Setting this property replaces the path nodes with nodes reconstructed from the segments.
///
/// - Precondition: Setting an empty array is an error.
@property (nonatomic, strong) NSArray<GSPathSegment *> *segments;

/// The path represented as path segments without collecting path object information.
@property (nonatomic, readonly) NSArray<GSPathSegment *> *segmentsFast;

/// The segment associated with a node index.
///
/// Off-curve node indices resolve to the following on-curve node.
///
/// Returns `nil` when no segment can be resolved for the index.
- (nullable GSPathSegment *)segmentAtNodeIndex:(NSInteger)idx;

/// The segment associated with a node index and the node index range it covers.
///
/// Off-curve node indices resolve to the following on-curve node.
///
/// Returns `nil` when no segment can be resolved for the index.
///
/// - Parameter segmentStartIdx: On return, the index of the first node in the segment.
/// - Parameter segmentEndIdx: On return, the index of the ending on-curve node.
- (nullable GSPathSegment *)segmentAtNodeIndex:(NSInteger)idx segmentStartIndex:(NSInteger *_Nullable)segmentStartIdx segmentEndIndex:(NSInteger *_Nullable)segmentEndIdx NS_REFINED_FOR_SWIFT;

/// An index from which every segment can be traversed in node order.
///
/// For a closed path, the index may be negative to include a segment crossing the array boundary.
///
/// Returns `0` for open paths and paths without on-curve nodes.
- (NSInteger)firstOncurveNodeIndex;

/// The next on-curve node after an index.
///
/// For a closed path, navigation wraps around the path.
/// For an open path, returns `nil` when no next on-curve node exists.
/// For a path consisting solely of off-curve nodes, returns `nil`.
- (nullable GSNode *)nextOncurveNodeFromIndex:(NSInteger)index;

/// The previous on-curve node before an index.
///
/// For a closed path, navigation wraps around the path.
/// For an open path, returns `nil` when no previous on-curve node exists.
/// For a path consisting solely of off-curve nodes, returns `nil`.
- (nullable GSNode *)previousOncurveNodeFromIndex:(NSInteger)index;

#ifndef GLYPHS_LITE
#ifndef LIBCORE
/// A geometric hash for matching paths with the same node structure.
- (GSPathHash *)compareHash;

/// Enumerates matching node positions for a geometric hash.
///
/// The callback receives the matching node index, the average angle, and the average position.
/// Set `stop` to `YES` in the callback to stop enumeration.
- (void)nodeIndexForHash:(GSPathHash *)hash callback:(void (^)(NSInteger nodeIdx, CGFloat averageAngle, NSPoint averagePos, BOOL *stop))callbackBlock;

/// A transform that maps a geometric hash to the matching position on the path.
///
/// Returns `nil` when the hash does not match the path.
- (nullable NSAffineTransform *)transformForHash:(GSPathHash *)hash;
#endif
#endif

/// Intersections between the path and a line segment.
///
/// Returns an empty point array when there are no intersections.
- (FTPointArray *)intersectionsWithLineFrom:(NSPoint)startPoint to:(NSPoint)endPoint;

#pragma GSAutoCodeStart methods

#pragma GSAutoCodeEnd methods

@end
NS_ASSUME_NONNULL_END
