//
//  YYTextLayout.h
//  YYKit <https://github.com/ibireme/YYKit>
//
//  Created by ibireme on 15/3/3.
//  Copyright (c) 2015 ibireme.
//
//  This source code is licensed under the MIT-style license found in the
//  LICENSE file in the root directory of this source tree.
//

#import <UIKit/UIKit.h>
#import <CoreText/CoreText.h>

#if __has_include(<YYKit/YYKit.h>)
#import <YYKit/YYTextDebugOption.h>
#import <YYKit/YYTextLine.h>
#import <YYKit/YYTextInput.h>
#else
#import "YYTextDebugOption.h"
#import "YYTextLine.h"
#import "YYTextInput.h"
#endif

@protocol YYTextLinePositionModifier;

NS_ASSUME_NONNULL_BEGIN

/**
 The max text container size in layout.
 */
extern const CGSize YYTextContainerMaxSize;

/**
 The YYTextContainer class defines a region in which text is laid out.
 YYTextLayout class uses one or more YYTextContainer objects to generate layouts.
 
 A YYTextContainer defines rectangular regions (`size` and `insets`) or 
 nonrectangular shapes (`path`), and you can define exclusion paths inside the 
 text container's bounding rectangle so that text flows around the exclusion 
 path as it is laid out.
 
 All methods in this class is thread-safe.
 
 Example:
 
     ┌─────────────────────────────┐  <------- container
     │                             │
     │    asdfasdfasdfasdfasdfa   <------------ container insets
     │    asdfasdfa   asdfasdfa    │
     │    asdfas         asdasd    │
     │    asdfa        <----------------------- container exclusion path
     │    asdfas         adfasd    │
     │    asdfasdfa   asdfasdfa    │
     │    asdfasdfasdfasdfasdfa    │
     │                             │
     └─────────────────────────────┘
 */
@interface YYTextContainer : NSObject <NSCoding, NSCopying>

/// Creates a container with the specified size. @param size The size.
+ (instancetype)containerWithSize:(CGSize)size;

/// Creates a container with the specified size and insets. @param size The size. @param insets The text insets.
+ (instancetype)containerWithSize:(CGSize)size insets:(UIEdgeInsets)insets;

/// Creates a container with the specified path. @param size The path.
+ (instancetype)containerWithPath:(nullable UIBezierPath *)path;

/// The constrained size. (if the size is larger than YYTextContainerMaxSize, it will be clipped)
@property CGSize size;

/// The insets for constrained size. The inset value should not be negative. Default is UIEdgeInsetsZero.
@property UIEdgeInsets insets;

/// Custom constrained path. Set this property to ignore `size` and `insets`. Default is nil.
@property (nullable, copy) UIBezierPath *path;

/// An array of `UIBezierPath` for path exclusion. Default is nil.
@property (nullable, copy) NSArray<UIBezierPath *> *exclusionPaths;

/// Path line width. Default is 0;
@property CGFloat pathLineWidth;

/// YES:(PathFillEvenOdd) Text is filled in the area that would be painted if the path were given to CGContextEOFillPath.
/// NO: (PathFillWindingNumber) Text is fill in the area that would be painted if the path were given to CGContextFillPath.
/// Default is YES;
@property (getter=isPathFillEvenOdd) BOOL pathFillEvenOdd;

/// Whether the text is vertical form (may used for CJK text layout). Default is NO.
@property (getter=isVerticalForm) BOOL verticalForm;

/// Maximum number of rows, 0 means no limit. Default is 0.
@property NSUInteger maximumNumberOfRows;

/// The line truncation type, default is none.
@property YYTextTruncationType truncationType;

/// The truncation token. If nil, the layout will use "…" instead. Default is nil.
@property (nullable, copy) NSAttributedString *truncationToken;

/// This modifier is applied to the lines before the layout is completed,
/// give you a chance to modify the line position. Default is nil.
@property (nullable, copy) id<YYTextLinePositionModifier> linePositionModifier;
@end


/**
 The YYTextLinePositionModifier protocol declares the required method to modify
 the line position in text layout progress. See `YYTextLinePositionSimpleModifier` for example.
 */
@protocol YYTextLinePositionModifier <NSObject, NSCopying>
@required
/**
 This method will called before layout is completed. The method should be thread-safe.
 @param lines     An array of YYTextLine.
 @param text      The full text.
 @param container The layout container.
 */
- (void)modifyLines:(NSArray<YYTextLine *> *)lines fromText:(NSAttributedString *)text inContainer:(YYTextContainer *)container;
@end


/**
 A simple implementation of `YYTextLinePositionModifier`. It can fix each line's position
 to a specified value, lets each line of height be the same.
 */
@interface YYTextLinePositionSimpleModifier : NSObject <YYTextLinePositionModifier>
@property (assign) CGFloat fixedLineHeight; ///< The fixed line height (distance between two baseline).
@end



/**
 YYTextLayout class is a readonly class stores text layout result.
 All the property in this class is readonly, and should not be changed.
 The methods in this class is thread-safe (except some of the draw methods).
 
 example: (layout with a circle exclusion path)
 
     ┌──────────────────────────┐  <------ container
     │ [--------Line0--------]  │  <- Row0
     │ [--------Line1--------]  │  <- Row1
     │ [-Line2-]     [-Line3-]  │  <- Row2
     │ [-Line4]       [Line5-]  │  <- Row3
     │ [-Line6-]     [-Line7-]  │  <- Row4
     │ [--------Line8--------]  │  <- Row5
     │ [--------Line9--------]  │  <- Row6
     └──────────────────────────┘
 */
@interface YYTextLayout : NSObject <NSCoding>


#pragma mark - Generate text layout
///=============================================================================
/// @name Generate text layout
///=============================================================================

/**
 Generate a layout with the given container size and text.

 @param size The text container's size
 @param text The text (if nil, returns nil).
 @return A new layout, or nil when an error occurs.
*/
+ (nullable YYTextLayout *)layoutWithContainerSize:(CGSize)size
                                              text:(NSAttributedString *)text;

/**
 Generate a layout with the given container and text.
 
 @param container The text container (if nil, returns nil).
 @param text      The text (if nil, returns nil).
 @return A new layout, or nil when an error occurs.
 */
+ (nullable YYTextLayout *)layoutWithContainer:(YYTextContainer *)container
                                          text:(NSAttributedString *)text;

/**
 Generate a layout with the given container and text.
 
 @param container The text container (if nil, returns nil).
 @param text      The text (if nil, returns nil).
 @param range     The text range (if out of range, returns nil). If the
    length of the range is 0, it means the length is no limit.
 @return A new layout, or nil when an error occurs.
 */
+ (nullable YYTextLayout *)layoutWithContainer:(YYTextContainer *)container
                                          text:(NSAttributedString *)text
                                         range:(NSRange)range;

/**
 Generate layouts with the given containers and text.
 
 @param containers An array of YYTextContainer object (if nil, returns nil).
 @param text       The text (if nil, returns nil).
 @return An array of YYTextLayout object (the count is same as containers),
    or nil when an error occurs.
 */
+ (nullable NSArray<YYTextLayout *> *)layoutWithContainers:(NSArray<YYTextContainer *> *)containers
                                                      text:(NSAttributedString *)text;

/**
 Generate layouts with the given containers and text.
 
 @param containers An array of YYTextContainer object (if nil, returns nil).
 @param text       The text (if nil, returns nil).
 @param range      The text range (if out of range, returns nil). If the
    length of the range is 0, it means the length is no limit.
 @return An array of YYTextLayout object (the count is same as containers),
    or nil when an error occurs.
 */
+ (nullable NSArray<YYTextLayout *> *)layoutWithContainers:(NSArray<YYTextContainer *> *)containers
                                                      text:(NSAttributedString *)text
                                                     range:(NSRange)range;

- (instancetype)init UNAVAILABLE_ATTRIBUTE;
+ (instancetype)new UNAVAILABLE_ATTRIBUTE;


#pragma mark - Text layout attributes
///=============================================================================
/// @name Text layout attributes
///=============================================================================

///< The text container
@property (nonatomic, strong, readonly) YYTextContainer *container;
///< The full text
@property (nonatomic, strong, readonly) NSAttributedString *text;
///< The text range in full text
@property (nonatomic, readonly) NSRange range;
///< CTFrameSetter
@property (nonatomic, readonly) CTFramesetterRef frameSetter;
///< CTFrame
@property (nonatomic, readonly) CTFrameRef frame;
///< Array of `YYTextLine`, no truncated
@property (nonatomic, strong, readonly) NSArray<YYTextLine *> *lines;
///< YYTextLine with truncated token, or nil
@property (nullable, nonatomic, strong, readonly) YYTextLine *truncatedLine;
///< Array of `YYTextAttachment`
@property (nullable, nonatomic, strong, readonly) NSArray<YYTextAttachment *> *attachments;
///< Array of NSRange(wrapped by NSValue) in text
@property (nullable, nonatomic, strong, readonly) NSArray<NSValue *> *attachmentRanges;
///< Array of CGRect(wrapped by NSValue) in container
@property (nullable, nonatomic, strong, readonly) NSArray<NSValue *> *attachmentRects;
///< Set of Attachment (UIImage/UIView/CALayer)
@property (nullable, nonatomic, strong, readonly) NSSet *attachmentContentsSet;
///< Number of rows
@property (nonatomic, readonly) NSUInteger rowCount;
///< Visible text range
@property (nonatomic, readonly) NSRange visibleRange;
///< Bounding rect (glyphs)
@property (nonatomic, readonly) CGRect textBoundingRect;
///< Bounding size (glyphs and insets, ceil to pixel)
@property (nonatomic, readonly) CGSize textBoundingSize;
///< Has highlight attribute
@property (nonatomic, readonly) BOOL containsHighlight;
///< Has block border attribute
@property (nonatomic, readonly) BOOL needDrawBlockBorder;
///< Has background border attribute
@property (nonatomic, readonly) BOOL needDrawBackgroundBorder;
///< Has shadow attribute
@property (nonatomic, readonly) BOOL needDrawShadow;
///< Has underline attribute
@property (nonatomic, readonly) BOOL needDrawUnderline;
///< Has visible text
@property (nonatomic, readonly) BOOL needDrawText;
///< Has attachment attribute
@property (nonatomic, readonly) BOOL needDrawAttachment;
///< Has inner shadow attribute
@property (nonatomic, readonly) BOOL needDrawInnerShadow;
///< Has strickthrough attribute
@property (nonatomic, readonly) BOOL needDrawStrikethrough;
///< Has border attribute
@property (nonatomic, readonly) BOOL needDrawBorder;


#pragma mark - Query information from text layout
///=============================================================================
/// @name Query information from text layout
///=============================================================================

/**
 The first line index for row.
 
 @param row  A row index.
 @return The line index, or NSNotFound if not found.
 */
- (NSUInteger)lineIndexForRow:(NSUInteger)row;

/**
 The number of lines for row.
 
 @param row  A row index.
 @return The number of lines, or NSNotFound when an error occurs.
 */
- (NSUInteger)lineCountForRow:(NSUInteger)row;

/**
 The row index for line.
 
 @param line A row index.
 
 @return The row index, or NSNotFound if not found.
 */
- (NSUInteger)rowIndexForLine:(NSUInteger)line;

/**
 The line index for a specified point.
 
 @discussion It returns NSNotFound if there's no text at the point.
 
 @param point  A point in the container.
 @return The line index, or NSNotFound if not found.
 */
- (NSUInteger)lineIndexForPoint:(CGPoint)point;

/**
 The line index closest to a specified point.
 
 @param point  A point in the container.
 @return The line index, or NSNotFound if no line exist in layout.
 */
- (NSUInteger)closestLineIndexForPoint:(CGPoint)point;

/**
 The offset in container for a text position in a specified line.
 
 @discussion The offset is the text position's baseline point.x.
 If the container is vertical form, the offset is the baseline point.y;
 
 @param position   The text position in string.
 @param lineIndex  The line index.
 @return The offset in container, or CGFLOAT_MAX if not found.
 */
- (CGFloat)offsetForTextPosition:(NSUInteger)position lineIndex:(NSUInteger)lineIndex;

/**
 The text position for a point in a specified line.
 
 @discussion This method just call CTLineGetStringIndexForPosition() and does 
 NOT consider the emoji, line break character, binding text...
 
 @param point      A point in the container.
 @param lineIndex  The line index.
 @return The text position, or NSNotFound if not found.
 */
- (NSUInteger)textPositionForPoint:(CGPoint)point lineIndex:(NSUInteger)lineIndex;

/**
 The closest text position to a specified point.
 
 @discussion This method takes into account the restrict of emoji, line break 
 character, binding text and text affinity.
 
 @param point  A point in the container.
 @return A text position, or nil if not found.
 */
- (nullable YYTextPosition *)closestPositionToPoint:(CGPoint)point;

/**
 Returns the new position when moving selection grabber in text view.
 
 @discussion There are two grabber in the text selection period, user can only 
 move one grabber at the same time.
 
 @param point          A point in the container.
 @param oldPosition    The old text position for the moving grabber.
 @param otherPosition  The other position in text selection view.
 
 @return A text position, or nil if not found.
 */
- (nullable YYTextPosition *)positionForPoint:(CGPoint)point
                                  oldPosition:(YYTextPosition *)oldPosition
                                otherPosition:(YYTextPosition *)otherPosition;

/**
 Returns the character or range of characters that is at a given point in the container.
 If there is no text at the point, returns nil.
 
 @discussion This method takes into account the restrict of emoji, line break
 character, binding text and text affinity.
 
 @param point  A point in the container.
 @return An object representing a range that encloses a character (or characters) 
 at point. Or nil if not found.
 */
- (nullable YYTextRange *)textRangeAtPoint:(CGPoint)point;

/**
 Returns the closest character or range of characters that is at a given point in 
 the container.
 
 @discussion This method takes into account the restrict of emoji, line break
 character, binding text and text affinity.
 
 @param point  A point in the container.
 @return An object representing a range that encloses a character (or characters)
 at point. Or nil if not found.
 */
- (nullable YYTextRange *)closestTextRangeAtPoint:(CGPoint)point;

/**
 If the position is inside an emoji, composed character sequences, line break '\\r\\n'
 or custom binding range, then returns the range by extend the position. Otherwise,
 returns a zero length range from the position.
 
 @param position A text-position object that identifies a location in layout.
 
 @return A text-range object that extend the position. Or nil if an error occurs
 */
- (nullable YYTextRange *)textRangeByExtendingPosition:(YYTextPosition *)position;

/**
 Returns a text range at a given offset in a specified direction from another 
 text position to its farthest extent in a certain direction of layout.
 
 @param position  A text-position object that identifies a location in layout.
 @param direction A constant that indicates a direction of layout (right, left, up, down).
 @param offset    A character offset from position.
 
 @return A text-range object that represents the distance from position to the
 farthest extent in direction. Or nil if an error occurs.
 */
- (nullable YYTextRange *)textRangeByExtendingPosition:(YYTextPosition *)position
                                           inDirection:(UITextLayoutDirection)direction
                                                offset:(NSInteger)offset;

/**
 Returns the line index for a given text position.
 
 @discussion This method takes into account the text affinity.
 
 @param position A text-position object that identifies a location in layout.
 @return The line index, or NSNotFound if not found.
 */
- (NSUInteger)lineIndexForPosition:(YYTextPosition *)position;

/**
 Returns the baseline position for a given text position.
 
 @param position An object that identifies a location in the layout.
 @return The baseline position for text, or CGPointZero if not found.
 */
- (CGPoint)linePositionForPosition:(YYTextPosition *)position;

/**
 Returns a rectangle used to draw the caret at a given insertion point.
 
 @param position An object that identifies a location in the layout.
 @return A rectangle that defines the area for drawing the caret. The width is
 always zero in normal container, the height is always zero in vertical form container.
 If not found, it returns CGRectNull.
 */
- (CGRect)caretRectForPosition:(YYTextPosition *)position;

/**
 Returns the first rectangle that encloses a range of text in the layout.
 
 @param range An object that represents a range of text in layout.
 
 @return The first rectangle in a range of text. You might use this rectangle to 
 draw a correction rectangle. The "first" in the name refers the rectangle 
 enclosing the first line when the range encompasses multiple lines of text.
 If not found, it returns CGRectNull.
 */
- (CGRect)firstRectForRange:(YYTextRange *)range;

/**
 Returns the rectangle union that encloses a range of text in the layout.
 
 @param range An object that represents a range of text in layout.
 
 @return A rectangle that defines the area than encloses the range.
 If not found, it returns CGRectNull.
 */
- (CGRect)rectForRange:(YYTextRange *)range;

/**
 Returns an array of selection rects corresponding to the range of text.
 The start and end rect can be used to show grabber.
 
 @param range An object representing a range in text.
 @return An array of `YYTextSelectionRect` objects that encompass the selection.
 If not found, the array is empty.
 */
- (NSArray<YYTextSelectionRect *> *)selectionRectsForRange:(YYTextRange *)range;

/**
 Returns an array of selection rects corresponding to the range of text.
 
 @param range An object representing a range in text.
 @return An array of `YYTextSelectionRect` objects that encompass the selection.
 If not found, the array is empty.
 */
- (NSArray<YYTextSelectionRect *> *)selectionRectsWithoutStartAndEndForRange:(YYTextRange *)range;

/**
 Returns the start and end selection rects corresponding to the range of text.
 The start and end rect can be used to show grabber.
 
 @param range An object representing a range in text.
 @return An array of `YYTextSelectionRect` objects contains the start and end to
 the selection. If not found, the array is empty.
 */
- (NSArray<YYTextSelectionRect *> *)selectionRectsWithOnlyStartAndEndForRange:(YYTextRange *)range;


#pragma mark - Draw text layout
///=============================================================================
/// @name Draw text layout
///=============================================================================

/**
 Draw the layout and show the attachments.
 
 @discussion If the `view` parameter is not nil, then the attachment views will
 add to this `view`, and if the `layer` parameter is not nil, then the attachment
 layers will add to this `layer`. 
 
 @warning This method should be called on main thread if `view` or `layer` parameter
 is not nil and there's UIView or CALayer attachments in layout. 
 Otherwise, it can be called on any thread.
 
 @param context The draw context. Pass nil to avoid text and image drawing.
 @param size    The context size.
 @param point   The point at which to draw the layout.
 @param view    The attachment views will add to this view.
 @param layer   The attachment layers will add to this layer.
 @param debug   The debug option. Pass nil to avoid debug drawing.
 @param cancel  The cancel checker block. It will be called in drawing progress.
                    If it returns YES, the further draw progress will be canceled.
                    Pass nil to ignore this feature.
 */
- (void)drawInContext:(nullable CGContextRef)context
                 size:(CGSize)size
                point:(CGPoint)point
                 view:(nullable UIView *)view
                layer:(nullable CALayer *)layer
                debug:(nullable YYTextDebugOption *)debug
               cancel:(nullable BOOL (^)(void))cancel;

/**
 Draw the layout text and image (without view or layer attachments).
 
 @discussion This method is thread safe and can be called on any thread.
 
 @param context The draw context. Pass nil to avoid text and image drawing.
 @param size    The context size.
 @param debug   The debug option. Pass nil to avoid debug drawing.
 */
- (void)drawInContext:(nullable CGContextRef)context
                 size:(CGSize)size
                debug:(nullable YYTextDebugOption *)debug;
/**
 Show view and layer attachments.
 
 @warning This method must be called on main thread.
 
 @param view  The attachment views will add to this view.
 @param layer The attachment layers will add to this layer.
 */
- (void)addAttachmentToView:(nullable UIView *)view layer:(nullable CALayer *)layer;

/**
 Remove attachment views and layers from their super container.
 
 @warning This method must be called on main thread.
 */
- (void)removeAttachmentFromViewAndLayer;

@end

NS_ASSUME_NONNULL_END
