//
//  NSAttributedString+ASText.mm
//  Texture
//
//  Copyright (c) Pinterest, Inc.  All rights reserved.
//  Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//

#import <AsyncDisplayKit/NSAttributedString+ASText.h>
#import <AsyncDisplayKit/NSParagraphStyle+ASText.h>
#import <AsyncDisplayKit/ASTextRunDelegate.h>
#import <AsyncDisplayKit/ASTextUtilities.h>
#import <CoreFoundation/CoreFoundation.h>


// Dummy class for category
@interface NSAttributedString_ASText : NSObject @end
@implementation NSAttributedString_ASText @end


@implementation NSAttributedString (ASText)

- (NSDictionary *)as_attributesAtIndex:(NSUInteger)index {
  if (index > self.length || self.length == 0) return nil;
  if (self.length > 0 && index == self.length) index--;
  return [self attributesAtIndex:index effectiveRange:NULL];
}

- (id)as_attribute:(NSString *)attributeName atIndex:(NSUInteger)index {
  if (!attributeName) return nil;
  if (index > self.length || self.length == 0) return nil;
  if (self.length > 0 && index == self.length) index--;
  return [self attribute:attributeName atIndex:index effectiveRange:NULL];
}

- (NSDictionary *)as_attributes {
  return [self as_attributesAtIndex:0];
}

- (UIFont *)as_font {
  return [self as_fontAtIndex:0];
}

- (UIFont *)as_fontAtIndex:(NSUInteger)index {
  return [self as_attribute:NSFontAttributeName atIndex:index];
}

- (NSNumber *)as_kern {
  return [self as_kernAtIndex:0];
}

- (NSNumber *)as_kernAtIndex:(NSUInteger)index {
  return [self as_attribute:NSKernAttributeName atIndex:index];
}

- (UIColor *)as_color {
  return [self as_colorAtIndex:0];
}

- (UIColor *)as_colorAtIndex:(NSUInteger)index {
  UIColor *color = [self as_attribute:NSForegroundColorAttributeName atIndex:index];
  if (!color) {
    CGColorRef ref = (__bridge CGColorRef)([self as_attribute:(NSString *)kCTForegroundColorAttributeName atIndex:index]);
    color = [UIColor colorWithCGColor:ref];
  }
  if (color && ![color isKindOfClass:[UIColor class]]) {
    if (CFGetTypeID((__bridge CFTypeRef)(color)) == CGColorGetTypeID()) {
      color = [UIColor colorWithCGColor:(__bridge CGColorRef)(color)];
    } else {
      color = nil;
    }
  }
  return color;
}

- (UIColor *)as_backgroundColor {
  return [self as_backgroundColorAtIndex:0];
}

- (UIColor *)as_backgroundColorAtIndex:(NSUInteger)index {
  return [self as_attribute:NSBackgroundColorAttributeName atIndex:index];
}

- (NSNumber *)as_strokeWidth {
  return [self as_strokeWidthAtIndex:0];
}

- (NSNumber *)as_strokeWidthAtIndex:(NSUInteger)index {
  return [self as_attribute:NSStrokeWidthAttributeName atIndex:index];
}

- (UIColor *)as_strokeColor {
  return [self as_strokeColorAtIndex:0];
}

- (UIColor *)as_strokeColorAtIndex:(NSUInteger)index {
  UIColor *color = [self as_attribute:NSStrokeColorAttributeName atIndex:index];
  if (!color) {
    CGColorRef ref = (__bridge CGColorRef)([self as_attribute:(NSString *)kCTStrokeColorAttributeName atIndex:index]);
    color = [UIColor colorWithCGColor:ref];
  }
  return color;
}

- (NSShadow *)as_shadow {
  return [self as_shadowAtIndex:0];
}

- (NSShadow *)as_shadowAtIndex:(NSUInteger)index {
  return [self as_attribute:NSShadowAttributeName atIndex:index];
}

- (NSUnderlineStyle)as_strikethroughStyle {
  return [self as_strikethroughStyleAtIndex:0];
}

- (NSUnderlineStyle)as_strikethroughStyleAtIndex:(NSUInteger)index {
  NSNumber *style = [self as_attribute:NSStrikethroughStyleAttributeName atIndex:index];
  return (NSUnderlineStyle)style.integerValue;
}

- (UIColor *)as_strikethroughColor {
  return [self as_strikethroughColorAtIndex:0];
}

- (UIColor *)as_strikethroughColorAtIndex:(NSUInteger)index {
  return [self as_attribute:NSStrikethroughColorAttributeName atIndex:index];
}

- (NSUnderlineStyle)as_underlineStyle {
  return [self as_underlineStyleAtIndex:0];
}

- (NSUnderlineStyle)as_underlineStyleAtIndex:(NSUInteger)index {
  NSNumber *style = [self as_attribute:NSUnderlineStyleAttributeName atIndex:index];
  return (NSUnderlineStyle)style.integerValue;
}

- (UIColor *)as_underlineColor {
  return [self as_underlineColorAtIndex:0];
}

- (UIColor *)as_underlineColorAtIndex:(NSUInteger)index {
  UIColor *color = [self as_attribute:NSUnderlineColorAttributeName atIndex:index];
  if (!color) {
    CGColorRef ref = (__bridge CGColorRef)([self as_attribute:(NSString *)kCTUnderlineColorAttributeName atIndex:index]);
    color = [UIColor colorWithCGColor:ref];
  }
  return color;
}

- (NSNumber *)as_ligature {
  return [self as_ligatureAtIndex:0];
}

- (NSNumber *)as_ligatureAtIndex:(NSUInteger)index {
  return [self as_attribute:NSLigatureAttributeName atIndex:index];
}

- (NSString *)as_textEffect {
  return [self as_textEffectAtIndex:0];
}

- (NSString *)as_textEffectAtIndex:(NSUInteger)index {
  return [self as_attribute:NSTextEffectAttributeName atIndex:index];
}

- (NSNumber *)as_obliqueness {
  return [self as_obliquenessAtIndex:0];
}

- (NSNumber *)as_obliquenessAtIndex:(NSUInteger)index {
  return [self as_attribute:NSObliquenessAttributeName atIndex:index];
}

- (NSNumber *)as_expansion {
  return [self as_expansionAtIndex:0];
}

- (NSNumber *)as_expansionAtIndex:(NSUInteger)index {
  return [self as_attribute:NSExpansionAttributeName atIndex:index];
}

- (NSNumber *)as_baselineOffset {
  return [self as_baselineOffsetAtIndex:0];
}

- (NSNumber *)as_baselineOffsetAtIndex:(NSUInteger)index {
  return [self as_attribute:NSBaselineOffsetAttributeName atIndex:index];
}

- (BOOL)as_verticalGlyphForm {
  return [self as_verticalGlyphFormAtIndex:0];
}

- (BOOL)as_verticalGlyphFormAtIndex:(NSUInteger)index {
  NSNumber *num = [self as_attribute:NSVerticalGlyphFormAttributeName atIndex:index];
  return num.boolValue;
}

- (NSString *)as_language {
  return [self as_languageAtIndex:0];
}

- (NSString *)as_languageAtIndex:(NSUInteger)index {
  return [self as_attribute:(id)kCTLanguageAttributeName atIndex:index];
}

- (NSArray *)as_writingDirection {
  return [self as_writingDirectionAtIndex:0];
}

- (NSArray *)as_writingDirectionAtIndex:(NSUInteger)index {
  return [self as_attribute:(id)kCTWritingDirectionAttributeName atIndex:index];
}

- (NSParagraphStyle *)as_paragraphStyle {
  return [self as_paragraphStyleAtIndex:0];
}

- (NSParagraphStyle *)as_paragraphStyleAtIndex:(NSUInteger)index {
  /*
   NSParagraphStyle is NOT toll-free bridged to CTParagraphStyleRef.
   
   CoreText can use both NSParagraphStyle and CTParagraphStyleRef,
   but UILabel/UITextView can only use NSParagraphStyle.
   
   We use NSParagraphStyle in both CoreText and UIKit.
   */
  NSParagraphStyle *style = [self as_attribute:NSParagraphStyleAttributeName atIndex:index];
  if (style) {
    if (CFGetTypeID((__bridge CFTypeRef)(style)) == CTParagraphStyleGetTypeID()) { \
      style = [NSParagraphStyle as_styleWithCTStyle:(__bridge CTParagraphStyleRef)(style)];
    }
  }
  return style;
}

#define ParagraphAttribute(_attr_) \
NSParagraphStyle *style = self.as_paragraphStyle; \
if (!style) style = [NSParagraphStyle defaultParagraphStyle]; \
return style. _attr_;

#define ParagraphAttributeAtIndex(_attr_) \
NSParagraphStyle *style = [self as_paragraphStyleAtIndex:index]; \
if (!style) style = [NSParagraphStyle defaultParagraphStyle]; \
return style. _attr_;

- (NSTextAlignment)as_alignment {
  ParagraphAttribute(alignment);
}

- (NSLineBreakMode)as_lineBreakMode {
  ParagraphAttribute(lineBreakMode);
}

- (CGFloat)as_lineSpacing {
  ParagraphAttribute(lineSpacing);
}

- (CGFloat)as_paragraphSpacing {
  ParagraphAttribute(paragraphSpacing);
}

- (CGFloat)as_paragraphSpacingBefore {
  ParagraphAttribute(paragraphSpacingBefore);
}

- (CGFloat)as_firstLineHeadIndent {
  ParagraphAttribute(firstLineHeadIndent);
}

- (CGFloat)as_headIndent {
  ParagraphAttribute(headIndent);
}

- (CGFloat)as_tailIndent {
  ParagraphAttribute(tailIndent);
}

- (CGFloat)as_minimumLineHeight {
  ParagraphAttribute(minimumLineHeight);
}

- (CGFloat)as_maximumLineHeight {
  ParagraphAttribute(maximumLineHeight);
}

- (CGFloat)as_lineHeightMultiple {
  ParagraphAttribute(lineHeightMultiple);
}

- (NSWritingDirection)as_baseWritingDirection {
  ParagraphAttribute(baseWritingDirection);
}

- (float)as_hyphenationFactor {
  ParagraphAttribute(hyphenationFactor);
}

- (CGFloat)as_defaultTabInterval {
  ParagraphAttribute(defaultTabInterval);
}

- (NSArray *)as_tabStops {
  ParagraphAttribute(tabStops);
}

- (NSTextAlignment)as_alignmentAtIndex:(NSUInteger)index {
  ParagraphAttributeAtIndex(alignment);
}

- (NSLineBreakMode)as_lineBreakModeAtIndex:(NSUInteger)index {
  ParagraphAttributeAtIndex(lineBreakMode);
}

- (CGFloat)as_lineSpacingAtIndex:(NSUInteger)index {
  ParagraphAttributeAtIndex(lineSpacing);
}

- (CGFloat)as_paragraphSpacingAtIndex:(NSUInteger)index {
  ParagraphAttributeAtIndex(paragraphSpacing);
}

- (CGFloat)as_paragraphSpacingBeforeAtIndex:(NSUInteger)index {
  ParagraphAttributeAtIndex(paragraphSpacingBefore);
}

- (CGFloat)as_firstLineHeadIndentAtIndex:(NSUInteger)index {
  ParagraphAttributeAtIndex(firstLineHeadIndent);
}

- (CGFloat)as_headIndentAtIndex:(NSUInteger)index {
  ParagraphAttributeAtIndex(headIndent);
}

- (CGFloat)as_tailIndentAtIndex:(NSUInteger)index {
  ParagraphAttributeAtIndex(tailIndent);
}

- (CGFloat)as_minimumLineHeightAtIndex:(NSUInteger)index {
  ParagraphAttributeAtIndex(minimumLineHeight);
}

- (CGFloat)as_maximumLineHeightAtIndex:(NSUInteger)index {
  ParagraphAttributeAtIndex(maximumLineHeight);
}

- (CGFloat)as_lineHeightMultipleAtIndex:(NSUInteger)index {
  ParagraphAttributeAtIndex(lineHeightMultiple);
}

- (NSWritingDirection)as_baseWritingDirectionAtIndex:(NSUInteger)index {
  ParagraphAttributeAtIndex(baseWritingDirection);
}

- (float)as_hyphenationFactorAtIndex:(NSUInteger)index {
  ParagraphAttributeAtIndex(hyphenationFactor);
}

- (CGFloat)as_defaultTabIntervalAtIndex:(NSUInteger)index {
  ParagraphAttributeAtIndex(defaultTabInterval);
}

- (NSArray *)as_tabStopsAtIndex:(NSUInteger)index {
  ParagraphAttributeAtIndex(tabStops);
}

#undef ParagraphAttribute
#undef ParagraphAttributeAtIndex

- (ASTextShadow *)as_textShadow {
  return [self as_textShadowAtIndex:0];
}

- (ASTextShadow *)as_textShadowAtIndex:(NSUInteger)index {
  return [self as_attribute:ASTextShadowAttributeName atIndex:index];
}

- (ASTextShadow *)as_textInnerShadow {
  return [self as_textInnerShadowAtIndex:0];
}

- (ASTextShadow *)as_textInnerShadowAtIndex:(NSUInteger)index {
  return [self as_attribute:ASTextInnerShadowAttributeName atIndex:index];
}

- (ASTextDecoration *)as_textUnderline {
  return [self as_textUnderlineAtIndex:0];
}

- (ASTextDecoration *)as_textUnderlineAtIndex:(NSUInteger)index {
  return [self as_attribute:ASTextUnderlineAttributeName atIndex:index];
}

- (ASTextDecoration *)as_textStrikethrough {
  return [self as_textStrikethroughAtIndex:0];
}

- (ASTextDecoration *)as_textStrikethroughAtIndex:(NSUInteger)index {
  return [self as_attribute:ASTextStrikethroughAttributeName atIndex:index];
}

- (ASTextBorder *)as_textBorder {
  return [self as_textBorderAtIndex:0];
}

- (ASTextBorder *)as_textBorderAtIndex:(NSUInteger)index {
  return [self as_attribute:ASTextBorderAttributeName atIndex:index];
}

- (ASTextBorder *)as_textBackgroundBorder {
  return [self as_textBackgroundBorderAtIndex:0];
}

- (ASTextBorder *)as_textBackgroundBorderAtIndex:(NSUInteger)index {
  return [self as_attribute:ASTextBackedStringAttributeName atIndex:index];
}

- (CGAffineTransform)as_textGlyphTransform {
  return [self as_textGlyphTransformAtIndex:0];
}

- (CGAffineTransform)as_textGlyphTransformAtIndex:(NSUInteger)index {
  NSValue *value = [self as_attribute:ASTextGlyphTransformAttributeName atIndex:index];
  if (!value) return CGAffineTransformIdentity;
  return [value CGAffineTransformValue];
}

- (NSString *)as_plainTextForRange:(NSRange)range {
  if (range.location == NSNotFound ||range.length == NSNotFound) return nil;
  NSMutableString *result = [NSMutableString string];
  if (range.length == 0) return result;
  NSString *string = self.string;
  [self enumerateAttribute:ASTextBackedStringAttributeName inRange:range options:kNilOptions usingBlock:^(id value, NSRange range, BOOL *stop) {
    ASTextBackedString *backed = value;
    if (backed && backed.string) {
      [result appendString:backed.string];
    } else {
      [result appendString:[string substringWithRange:range]];
    }
  }];
  return result;
}

+ (NSMutableAttributedString *)as_attachmentStringWithContent:(id)content
                                                  contentMode:(UIViewContentMode)contentMode
                                                        width:(CGFloat)width
                                                       ascent:(CGFloat)ascent
                                                      descent:(CGFloat)descent {
  NSMutableAttributedString *atr = [[NSMutableAttributedString alloc] initWithString:ASTextAttachmentToken];
  
  ASTextAttachment *attach = [ASTextAttachment new];
  attach.content = content;
  attach.contentMode = contentMode;
  [atr as_setTextAttachment:attach range:NSMakeRange(0, atr.length)];
  
  ASTextRunDelegate *delegate = [ASTextRunDelegate new];
  delegate.width = width;
  delegate.ascent = ascent;
  delegate.descent = descent;
  CTRunDelegateRef delegateRef = delegate.CTRunDelegate;
  [atr as_setRunDelegate:delegateRef range:NSMakeRange(0, atr.length)];
  if (delegate) CFRelease(delegateRef);
  
  return atr;
}

+ (NSMutableAttributedString *)as_attachmentStringWithContent:(id)content
                                                  contentMode:(UIViewContentMode)contentMode
                                               attachmentSize:(CGSize)attachmentSize
                                                  alignToFont:(UIFont *)font
                                                    alignment:(ASTextVerticalAlignment)alignment {
  NSMutableAttributedString *atr = [[NSMutableAttributedString alloc] initWithString:ASTextAttachmentToken];
  
  ASTextAttachment *attach = [ASTextAttachment new];
  attach.content = content;
  attach.contentMode = contentMode;
  [atr as_setTextAttachment:attach range:NSMakeRange(0, atr.length)];
  
  ASTextRunDelegate *delegate = [ASTextRunDelegate new];
  delegate.width = attachmentSize.width;
  switch (alignment) {
    case ASTextVerticalAlignmentTop: {
      delegate.ascent = font.ascender;
      delegate.descent = attachmentSize.height - font.ascender;
      if (delegate.descent < 0) {
        delegate.descent = 0;
        delegate.ascent = attachmentSize.height;
      }
    } break;
    case ASTextVerticalAlignmentCenter: {
      CGFloat fontHeight = font.ascender - font.descender;
      CGFloat yOffset = font.ascender - fontHeight * 0.5;
      delegate.ascent = attachmentSize.height * 0.5 + yOffset;
      delegate.descent = attachmentSize.height - delegate.ascent;
      if (delegate.descent < 0) {
        delegate.descent = 0;
        delegate.ascent = attachmentSize.height;
      }
    } break;
    case ASTextVerticalAlignmentBottom: {
      delegate.ascent = attachmentSize.height + font.descender;
      delegate.descent = -font.descender;
      if (delegate.ascent < 0) {
        delegate.ascent = 0;
        delegate.descent = attachmentSize.height;
      }
    } break;
    default: {
      delegate.ascent = attachmentSize.height;
      delegate.descent = 0;
    } break;
  }
  
  CTRunDelegateRef delegateRef = delegate.CTRunDelegate;
  [atr as_setRunDelegate:delegateRef range:NSMakeRange(0, atr.length)];
  if (delegate) CFRelease(delegateRef);
  
  return atr;
}

+ (NSMutableAttributedString *)as_attachmentStringWithEmojiImage:(UIImage *)image
                                                        fontSize:(CGFloat)fontSize {
  if (!image || fontSize <= 0) return nil;
  
  BOOL hasAnim = NO;
  if (image.images.count > 1) {
    hasAnim = YES;
  } else if (NSProtocolFromString(@"ASAnimatedImage") &&
             [image conformsToProtocol:NSProtocolFromString(@"ASAnimatedImage")]) {
    NSNumber *frameCount = [image valueForKey:@"animatedImageFrameCount"];
    if (frameCount.intValue > 1) hasAnim = YES;
  }
  
  CGFloat ascent = ASTextEmojiGetAscentWithFontSize(fontSize);
  CGFloat descent = ASTextEmojiGetDescentWithFontSize(fontSize);
  CGRect bounding = ASTextEmojiGetGlyphBoundingRectWithFontSize(fontSize);
  
  ASTextRunDelegate *delegate = [ASTextRunDelegate new];
  delegate.ascent = ascent;
  delegate.descent = descent;
  delegate.width = bounding.size.width + 2 * bounding.origin.x;
  
  ASTextAttachment *attachment = [ASTextAttachment new];
  attachment.contentMode = UIViewContentModeScaleAspectFit;
  attachment.contentInsets = UIEdgeInsetsMake(ascent - (bounding.size.height + bounding.origin.y), bounding.origin.x, descent + bounding.origin.y, bounding.origin.x);
  if (hasAnim) {
    Class imageClass = NSClassFromString(@"ASAnimatedImageView");
    if (!imageClass) imageClass = [UIImageView class];
    UIImageView *view = (id)[imageClass new];
    view.frame = bounding;
    view.image = image;
    view.contentMode = UIViewContentModeScaleAspectFit;
    attachment.content = view;
  } else {
    attachment.content = image;
  }
  
  NSMutableAttributedString *atr = [[NSMutableAttributedString alloc] initWithString:ASTextAttachmentToken];
  [atr as_setTextAttachment:attachment range:NSMakeRange(0, atr.length)];
  CTRunDelegateRef ctDelegate = delegate.CTRunDelegate;
  [atr as_setRunDelegate:ctDelegate range:NSMakeRange(0, atr.length)];
  if (ctDelegate) CFRelease(ctDelegate);
  
  return atr;
}

- (NSRange)as_rangeOfAll {
  return NSMakeRange(0, self.length);
}

- (BOOL)as_isSharedAttributesInAllRange {
  __block BOOL shared = YES;
  __block NSDictionary *firstAttrs = nil;
  [self enumerateAttributesInRange:self.as_rangeOfAll options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:^(NSDictionary *attrs, NSRange range, BOOL *stop) {
    if (range.location == 0) {
      firstAttrs = attrs;
    } else {
      if (firstAttrs.count != attrs.count) {
        shared = NO;
        *stop = YES;
      } else if (firstAttrs) {
        if (![firstAttrs isEqualToDictionary:attrs]) {
          shared = NO;
          *stop = YES;
        }
      }
    }
  }];
  return shared;
}

- (BOOL)as_canDrawWithUIKit {
  static NSMutableSet *failSet;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    failSet = [NSMutableSet new];
    [failSet addObject:(id)kCTGlyphInfoAttributeName];
#if TARGET_OS_IOS
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
    [failSet addObject:(id)kCTCharacterShapeAttributeName];
#pragma clang diagnostic pop
#endif
    [failSet addObject:(id)kCTLanguageAttributeName];
    [failSet addObject:(id)kCTRunDelegateAttributeName];
    [failSet addObject:(id)kCTBaselineClassAttributeName];
    [failSet addObject:(id)kCTBaselineInfoAttributeName];
    [failSet addObject:(id)kCTBaselineReferenceInfoAttributeName];
    [failSet addObject:(id)kCTRubyAnnotationAttributeName];
    [failSet addObject:ASTextShadowAttributeName];
    [failSet addObject:ASTextInnerShadowAttributeName];
    [failSet addObject:ASTextUnderlineAttributeName];
    [failSet addObject:ASTextStrikethroughAttributeName];
    [failSet addObject:ASTextBorderAttributeName];
    [failSet addObject:ASTextBackgroundBorderAttributeName];
    [failSet addObject:ASTextBlockBorderAttributeName];
    [failSet addObject:ASTextAttachmentAttributeName];
    [failSet addObject:ASTextHighlightAttributeName];
    [failSet addObject:ASTextGlyphTransformAttributeName];
  });
  
#define Fail { result = NO; *stop = YES; return; }
  __block BOOL result = YES;
  [self enumerateAttributesInRange:self.as_rangeOfAll options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:^(NSDictionary *attrs, NSRange range, BOOL *stop) {
    if (attrs.count == 0) return;
    for (NSString *str in attrs.allKeys) {
      if ([failSet containsObject:str]) Fail;
    }
    if (attrs[(id)kCTForegroundColorAttributeName] && !attrs[NSForegroundColorAttributeName]) Fail;
    if (attrs[(id)kCTStrokeColorAttributeName] && !attrs[NSStrokeColorAttributeName]) Fail;
    if (attrs[(id)kCTUnderlineColorAttributeName]) {
      if (!attrs[NSUnderlineColorAttributeName]) Fail;
    }
    NSParagraphStyle *style = attrs[NSParagraphStyleAttributeName];
    if (style && CFGetTypeID((__bridge CFTypeRef)(style)) == CTParagraphStyleGetTypeID()) Fail;
  }];
  return result;
#undef Fail
}

@end

@implementation NSMutableAttributedString (ASText)

- (void)as_setAttributes:(NSDictionary *)attributes {
  [self setAs_attributes:attributes];
}

- (void)setAs_attributes:(NSDictionary *)attributes {
  if (attributes == (id)[NSNull null]) attributes = nil;
  [self setAttributes:@{} range:NSMakeRange(0, self.length)];
  [attributes enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
    [self as_setAttribute:key value:obj];
  }];
}

- (void)as_setAttribute:(NSString *)name value:(id)value {
  [self as_setAttribute:name value:value range:NSMakeRange(0, self.length)];
}

- (void)as_setAttribute:(NSString *)name value:(id)value range:(NSRange)range {
  if (!name || [NSNull isEqual:name]) return;
  if (value && ![NSNull isEqual:value]) [self addAttribute:name value:value range:range];
  else [self removeAttribute:name range:range];
}

- (void)as_removeAttributesInRange:(NSRange)range {
  [self setAttributes:nil range:range];
}

#pragma mark - Property Setter

- (void)setAs_font:(UIFont *)font {
  /*
   In iOS7 and later, UIFont is toll-free bridged to CTFontRef,
   although Apple does not mention it in documentation.
   
   In iOS6, UIFont is a wrapper for CTFontRef, so CoreText can alse use UIfont,
   but UILabel/UITextView cannot use CTFontRef.
   
   We use UIFont for both CoreText and UIKit.
   */
  [self as_setFont:font range:NSMakeRange(0, self.length)];
}

- (void)setAs_kern:(NSNumber *)kern {
  [self as_setKern:kern range:NSMakeRange(0, self.length)];
}

- (void)setAs_color:(UIColor *)color {
  [self as_setColor:color range:NSMakeRange(0, self.length)];
}

- (void)setAs_backgroundColor:(UIColor *)backgroundColor {
  [self as_setBackgroundColor:backgroundColor range:NSMakeRange(0, self.length)];
}

- (void)setAs_strokeWidth:(NSNumber *)strokeWidth {
  [self as_setStrokeWidth:strokeWidth range:NSMakeRange(0, self.length)];
}

- (void)setAs_strokeColor:(UIColor *)strokeColor {
  [self as_setStrokeColor:strokeColor range:NSMakeRange(0, self.length)];
}

- (void)setAs_shadow:(NSShadow *)shadow {
  [self as_setShadow:shadow range:NSMakeRange(0, self.length)];
}

- (void)setAs_strikethroughStyle:(NSUnderlineStyle)strikethroughStyle {
  [self as_setStrikethroughStyle:strikethroughStyle range:NSMakeRange(0, self.length)];
}

- (void)setAs_strikethroughColor:(UIColor *)strikethroughColor {
  [self as_setStrikethroughColor:strikethroughColor range:NSMakeRange(0, self.length)];
}

- (void)setAs_underlineStyle:(NSUnderlineStyle)underlineStyle {
  [self as_setUnderlineStyle:underlineStyle range:NSMakeRange(0, self.length)];
}

- (void)setAs_underlineColor:(UIColor *)underlineColor {
  [self as_setUnderlineColor:underlineColor range:NSMakeRange(0, self.length)];
}

- (void)setAs_ligature:(NSNumber *)ligature {
  [self as_setLigature:ligature range:NSMakeRange(0, self.length)];
}

- (void)setAs_textEffect:(NSString *)textEffect {
  [self as_setTextEffect:textEffect range:NSMakeRange(0, self.length)];
}

- (void)setAs_obliqueness:(NSNumber *)obliqueness {
  [self as_setObliqueness:obliqueness range:NSMakeRange(0, self.length)];
}

- (void)setAs_expansion:(NSNumber *)expansion {
  [self as_setExpansion:expansion range:NSMakeRange(0, self.length)];
}

- (void)setAs_baselineOffset:(NSNumber *)baselineOffset {
  [self as_setBaselineOffset:baselineOffset range:NSMakeRange(0, self.length)];
}

- (void)setAs_verticalGlyphForm:(BOOL)verticalGlyphForm {
  [self as_setVerticalGlyphForm:verticalGlyphForm range:NSMakeRange(0, self.length)];
}

- (void)setAs_language:(NSString *)language {
  [self as_setLanguage:language range:NSMakeRange(0, self.length)];
}

- (void)setAs_writingDirection:(NSArray *)writingDirection {
  [self as_setWritingDirection:writingDirection range:NSMakeRange(0, self.length)];
}

- (void)setAs_paragraphStyle:(NSParagraphStyle *)paragraphStyle {
  /*
   NSParagraphStyle is NOT toll-free bridged to CTParagraphStyleRef.
   
   CoreText can use both NSParagraphStyle and CTParagraphStyleRef,
   but UILabel/UITextView can only use NSParagraphStyle.
   
   We use NSParagraphStyle in both CoreText and UIKit.
   */
  [self as_setParagraphStyle:paragraphStyle range:NSMakeRange(0, self.length)];
}

- (void)setAs_alignment:(NSTextAlignment)alignment {
  [self as_setAlignment:alignment range:NSMakeRange(0, self.length)];
}

- (void)setAs_baseWritingDirection:(NSWritingDirection)baseWritingDirection {
  [self as_setBaseWritingDirection:baseWritingDirection range:NSMakeRange(0, self.length)];
}

- (void)setAs_lineSpacing:(CGFloat)lineSpacing {
  [self as_setLineSpacing:lineSpacing range:NSMakeRange(0, self.length)];
}

- (void)setAs_paragraphSpacing:(CGFloat)paragraphSpacing {
  [self as_setParagraphSpacing:paragraphSpacing range:NSMakeRange(0, self.length)];
}

- (void)setAs_paragraphSpacingBefore:(CGFloat)paragraphSpacingBefore {
  [self as_setParagraphSpacing:paragraphSpacingBefore range:NSMakeRange(0, self.length)];
}

- (void)setAs_firstLineHeadIndent:(CGFloat)firstLineHeadIndent {
  [self as_setFirstLineHeadIndent:firstLineHeadIndent range:NSMakeRange(0, self.length)];
}

- (void)setAs_headIndent:(CGFloat)headIndent {
  [self as_setHeadIndent:headIndent range:NSMakeRange(0, self.length)];
}

- (void)setAs_tailIndent:(CGFloat)tailIndent {
  [self as_setTailIndent:tailIndent range:NSMakeRange(0, self.length)];
}

- (void)setAs_lineBreakMode:(NSLineBreakMode)lineBreakMode {
  [self as_setLineBreakMode:lineBreakMode range:NSMakeRange(0, self.length)];
}

- (void)setAs_minimumLineHeight:(CGFloat)minimumLineHeight {
  [self as_setMinimumLineHeight:minimumLineHeight range:NSMakeRange(0, self.length)];
}

- (void)setAs_maximumLineHeight:(CGFloat)maximumLineHeight {
  [self as_setMaximumLineHeight:maximumLineHeight range:NSMakeRange(0, self.length)];
}

- (void)setAs_lineHeightMultiple:(CGFloat)lineHeightMultiple {
  [self as_setLineHeightMultiple:lineHeightMultiple range:NSMakeRange(0, self.length)];
}

- (void)setAs_hyphenationFactor:(float)hyphenationFactor {
  [self as_setHyphenationFactor:hyphenationFactor range:NSMakeRange(0, self.length)];
}

- (void)setAs_defaultTabInterval:(CGFloat)defaultTabInterval {
  [self as_setDefaultTabInterval:defaultTabInterval range:NSMakeRange(0, self.length)];
}

- (void)setAs_tabStops:(NSArray *)tabStops {
  [self as_setTabStops:tabStops range:NSMakeRange(0, self.length)];
}

- (void)setAs_textShadow:(ASTextShadow *)textShadow {
  [self as_setTextShadow:textShadow range:NSMakeRange(0, self.length)];
}

- (void)setAs_textInnerShadow:(ASTextShadow *)textInnerShadow {
  [self as_setTextInnerShadow:textInnerShadow range:NSMakeRange(0, self.length)];
}

- (void)setAs_textUnderline:(ASTextDecoration *)textUnderline {
  [self as_setTextUnderline:textUnderline range:NSMakeRange(0, self.length)];
}

- (void)setAs_textStrikethrough:(ASTextDecoration *)textStrikethrough {
  [self as_setTextStrikethrough:textStrikethrough range:NSMakeRange(0, self.length)];
}

- (void)setAs_textBorder:(ASTextBorder *)textBorder {
  [self as_setTextBorder:textBorder range:NSMakeRange(0, self.length)];
}

- (void)setAs_textBackgroundBorder:(ASTextBorder *)textBackgroundBorder {
  [self as_setTextBackgroundBorder:textBackgroundBorder range:NSMakeRange(0, self.length)];
}

- (void)setAs_textGlyphTransform:(CGAffineTransform)textGlyphTransform {
  [self as_setTextGlyphTransform:textGlyphTransform range:NSMakeRange(0, self.length)];
}

#pragma mark - Range Setter

- (void)as_setFont:(UIFont *)font range:(NSRange)range {
  [self as_setAttribute:NSFontAttributeName value:font range:range];
}

- (void)as_setKern:(NSNumber *)kern range:(NSRange)range {
  [self as_setAttribute:NSKernAttributeName value:kern range:range];
}

- (void)as_setColor:(UIColor *)color range:(NSRange)range {
  [self as_setAttribute:(id)kCTForegroundColorAttributeName value:(id)color.CGColor range:range];
  [self as_setAttribute:NSForegroundColorAttributeName value:color range:range];
}

- (void)as_setBackgroundColor:(UIColor *)backgroundColor range:(NSRange)range {
  [self as_setAttribute:NSBackgroundColorAttributeName value:backgroundColor range:range];
}

- (void)as_setStrokeWidth:(NSNumber *)strokeWidth range:(NSRange)range {
  [self as_setAttribute:NSStrokeWidthAttributeName value:strokeWidth range:range];
}

- (void)as_setStrokeColor:(UIColor *)strokeColor range:(NSRange)range {
  [self as_setAttribute:(id)kCTStrokeColorAttributeName value:(id)strokeColor.CGColor range:range];
  [self as_setAttribute:NSStrokeColorAttributeName value:strokeColor range:range];
}

- (void)as_setShadow:(NSShadow *)shadow range:(NSRange)range {
  [self as_setAttribute:NSShadowAttributeName value:shadow range:range];
}

- (void)as_setStrikethroughStyle:(NSUnderlineStyle)strikethroughStyle range:(NSRange)range {
  NSNumber *style = strikethroughStyle == 0 ? nil : @(strikethroughStyle);
  [self as_setAttribute:NSStrikethroughStyleAttributeName value:style range:range];
}

- (void)as_setStrikethroughColor:(UIColor *)strikethroughColor range:(NSRange)range {
  [self as_setAttribute:NSStrikethroughColorAttributeName value:strikethroughColor range:range];
}

- (void)as_setUnderlineStyle:(NSUnderlineStyle)underlineStyle range:(NSRange)range {
  NSNumber *style = underlineStyle == 0 ? nil : @(underlineStyle);
  [self as_setAttribute:NSUnderlineStyleAttributeName value:style range:range];
}

- (void)as_setUnderlineColor:(UIColor *)underlineColor range:(NSRange)range {
  [self as_setAttribute:(id)kCTUnderlineColorAttributeName value:(id)underlineColor.CGColor range:range];
  [self as_setAttribute:NSUnderlineColorAttributeName value:underlineColor range:range];
}

- (void)as_setLigature:(NSNumber *)ligature range:(NSRange)range {
  [self as_setAttribute:NSLigatureAttributeName value:ligature range:range];
}

- (void)as_setTextEffect:(NSString *)textEffect range:(NSRange)range {
  [self as_setAttribute:NSTextEffectAttributeName value:textEffect range:range];
}

- (void)as_setObliqueness:(NSNumber *)obliqueness range:(NSRange)range {
  [self as_setAttribute:NSObliquenessAttributeName value:obliqueness range:range];
}

- (void)as_setExpansion:(NSNumber *)expansion range:(NSRange)range {
  [self as_setAttribute:NSExpansionAttributeName value:expansion range:range];
}

- (void)as_setBaselineOffset:(NSNumber *)baselineOffset range:(NSRange)range {
  [self as_setAttribute:NSBaselineOffsetAttributeName value:baselineOffset range:range];
}

- (void)as_setVerticalGlyphForm:(BOOL)verticalGlyphForm range:(NSRange)range {
  NSNumber *v = verticalGlyphForm ? @(YES) : nil;
  [self as_setAttribute:NSVerticalGlyphFormAttributeName value:v range:range];
}

- (void)as_setLanguage:(NSString *)language range:(NSRange)range {
  [self as_setAttribute:(id)kCTLanguageAttributeName value:language range:range];
}

- (void)as_setWritingDirection:(NSArray *)writingDirection range:(NSRange)range {
  [self as_setAttribute:(id)kCTWritingDirectionAttributeName value:writingDirection range:range];
}

- (void)as_setParagraphStyle:(NSParagraphStyle *)paragraphStyle range:(NSRange)range {
  /*
   NSParagraphStyle is NOT toll-free bridged to CTParagraphStyleRef.
   
   CoreText can use both NSParagraphStyle and CTParagraphStyleRef,
   but UILabel/UITextView can only use NSParagraphStyle.
   
   We use NSParagraphStyle in both CoreText and UIKit.
   */
  [self as_setAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range];
}

#define ParagraphStyleSet(_attr_) \
[self enumerateAttribute:NSParagraphStyleAttributeName \
inRange:range \
options:kNilOptions \
usingBlock: ^(NSParagraphStyle *value, NSRange subRange, BOOL *stop) { \
NSMutableParagraphStyle *style = nil; \
if (value) { \
if (CFGetTypeID((__bridge CFTypeRef)(value)) == CTParagraphStyleGetTypeID()) { \
value = [NSParagraphStyle as_styleWithCTStyle:(__bridge CTParagraphStyleRef)(value)]; \
} \
if (value. _attr_ == _attr_) return; \
if ([value isKindOfClass:[NSMutableParagraphStyle class]]) { \
style = (id)value; \
} else { \
style = value.mutableCopy; \
} \
} else { \
if ([NSParagraphStyle defaultParagraphStyle]. _attr_ == _attr_) return; \
style = [NSParagraphStyle defaultParagraphStyle].mutableCopy; \
} \
style. _attr_ = _attr_; \
[self as_setParagraphStyle:style range:subRange]; \
}];

- (void)as_setAlignment:(NSTextAlignment)alignment range:(NSRange)range {
  ParagraphStyleSet(alignment);
}

- (void)as_setBaseWritingDirection:(NSWritingDirection)baseWritingDirection range:(NSRange)range {
  ParagraphStyleSet(baseWritingDirection);
}

- (void)as_setLineSpacing:(CGFloat)lineSpacing range:(NSRange)range {
  ParagraphStyleSet(lineSpacing);
}

- (void)as_setParagraphSpacing:(CGFloat)paragraphSpacing range:(NSRange)range {
  ParagraphStyleSet(paragraphSpacing);
}

- (void)as_setParagraphSpacingBefore:(CGFloat)paragraphSpacingBefore range:(NSRange)range {
  ParagraphStyleSet(paragraphSpacingBefore);
}

- (void)as_setFirstLineHeadIndent:(CGFloat)firstLineHeadIndent range:(NSRange)range {
  ParagraphStyleSet(firstLineHeadIndent);
}

- (void)as_setHeadIndent:(CGFloat)headIndent range:(NSRange)range {
  ParagraphStyleSet(headIndent);
}

- (void)as_setTailIndent:(CGFloat)tailIndent range:(NSRange)range {
  ParagraphStyleSet(tailIndent);
}

- (void)as_setLineBreakMode:(NSLineBreakMode)lineBreakMode range:(NSRange)range {
  ParagraphStyleSet(lineBreakMode);
}

- (void)as_setMinimumLineHeight:(CGFloat)minimumLineHeight range:(NSRange)range {
  ParagraphStyleSet(minimumLineHeight);
}

- (void)as_setMaximumLineHeight:(CGFloat)maximumLineHeight range:(NSRange)range {
  ParagraphStyleSet(maximumLineHeight);
}

- (void)as_setLineHeightMultiple:(CGFloat)lineHeightMultiple range:(NSRange)range {
  ParagraphStyleSet(lineHeightMultiple);
}

- (void)as_setHyphenationFactor:(float)hyphenationFactor range:(NSRange)range {
  ParagraphStyleSet(hyphenationFactor);
}

- (void)as_setDefaultTabInterval:(CGFloat)defaultTabInterval range:(NSRange)range {
  ParagraphStyleSet(defaultTabInterval);
}

- (void)as_setTabStops:(NSArray *)tabStops range:(NSRange)range {
  ParagraphStyleSet(tabStops);
}

#undef ParagraphStyleSet

- (void)as_setSuperscript:(NSNumber *)superscript range:(NSRange)range {
  if ([superscript isEqualToNumber:@(0)]) {
    superscript = nil;
  }
  [self as_setAttribute:(id)kCTSuperscriptAttributeName value:superscript range:range];
}

- (void)as_setGlyphInfo:(CTGlyphInfoRef)glyphInfo range:(NSRange)range {
  [self as_setAttribute:(id)kCTGlyphInfoAttributeName value:(__bridge id)glyphInfo range:range];
}

- (void)as_setCharacterShape:(NSNumber *)characterShape range:(NSRange)range {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
  [self as_setAttribute:(id)kCTCharacterShapeAttributeName value:characterShape range:range];
#pragma clang diagnostic pop
}

- (void)as_setRunDelegate:(CTRunDelegateRef)runDelegate range:(NSRange)range {
  [self as_setAttribute:(id)kCTRunDelegateAttributeName value:(__bridge id)runDelegate range:range];
}

- (void)as_setBaselineClass:(CFStringRef)baselineClass range:(NSRange)range {
  [self as_setAttribute:(id)kCTBaselineClassAttributeName value:(__bridge id)baselineClass range:range];
}

- (void)as_setBaselineInfo:(CFDictionaryRef)baselineInfo range:(NSRange)range {
  [self as_setAttribute:(id)kCTBaselineInfoAttributeName value:(__bridge id)baselineInfo range:range];
}

- (void)as_setBaselineReferenceInfo:(CFDictionaryRef)referenceInfo range:(NSRange)range {
  [self as_setAttribute:(id)kCTBaselineReferenceInfoAttributeName value:(__bridge id)referenceInfo range:range];
}

- (void)as_setRubyAnnotation:(CTRubyAnnotationRef)ruby range:(NSRange)range {
  [self as_setAttribute:(id)kCTRubyAnnotationAttributeName value:(__bridge id)ruby range:range];
}

- (void)as_setAttachment:(NSTextAttachment *)attachment range:(NSRange)range {
  [self as_setAttribute:NSAttachmentAttributeName value:attachment range:range];
}

- (void)as_setLink:(id)link range:(NSRange)range {
  [self as_setAttribute:NSLinkAttributeName value:link range:range];
}

- (void)as_setTextBackedString:(ASTextBackedString *)textBackedString range:(NSRange)range {
  [self as_setAttribute:ASTextBackedStringAttributeName value:textBackedString range:range];
}

- (void)as_setTextBinding:(ASTextBinding *)textBinding range:(NSRange)range {
  [self as_setAttribute:ASTextBindingAttributeName value:textBinding range:range];
}

- (void)as_setTextShadow:(ASTextShadow *)textShadow range:(NSRange)range {
  [self as_setAttribute:ASTextShadowAttributeName value:textShadow range:range];
}

- (void)as_setTextInnerShadow:(ASTextShadow *)textInnerShadow range:(NSRange)range {
  [self as_setAttribute:ASTextInnerShadowAttributeName value:textInnerShadow range:range];
}

- (void)as_setTextUnderline:(ASTextDecoration *)textUnderline range:(NSRange)range {
  [self as_setAttribute:ASTextUnderlineAttributeName value:textUnderline range:range];
}

- (void)as_setTextStrikethrough:(ASTextDecoration *)textStrikethrough range:(NSRange)range {
  [self as_setAttribute:ASTextStrikethroughAttributeName value:textStrikethrough range:range];
}

- (void)as_setTextBorder:(ASTextBorder *)textBorder range:(NSRange)range {
  [self as_setAttribute:ASTextBorderAttributeName value:textBorder range:range];
}

- (void)as_setTextBackgroundBorder:(ASTextBorder *)textBackgroundBorder range:(NSRange)range {
  [self as_setAttribute:ASTextBackgroundBorderAttributeName value:textBackgroundBorder range:range];
}

- (void)as_setTextAttachment:(ASTextAttachment *)textAttachment range:(NSRange)range {
  [self as_setAttribute:ASTextAttachmentAttributeName value:textAttachment range:range];
}

- (void)as_setTextHighlight:(ASTextHighlight *)textHighlight range:(NSRange)range {
  [self as_setAttribute:ASTextHighlightAttributeName value:textHighlight range:range];
}

- (void)as_setTextBlockBorder:(ASTextBorder *)textBlockBorder range:(NSRange)range {
  [self as_setAttribute:ASTextBlockBorderAttributeName value:textBlockBorder range:range];
}

- (void)as_setTextGlyphTransform:(CGAffineTransform)textGlyphTransform range:(NSRange)range {
  NSValue *value = CGAffineTransformIsIdentity(textGlyphTransform) ? nil : [NSValue valueWithCGAffineTransform:textGlyphTransform];
  [self as_setAttribute:ASTextGlyphTransformAttributeName value:value range:range];
}

- (void)as_setTextHighlightRange:(NSRange)range
                           color:(UIColor *)color
                 backgroundColor:(UIColor *)backgroundColor
                        userInfo:(NSDictionary *)userInfo
                       tapAction:(ASTextAction)tapAction
                 longPressAction:(ASTextAction)longPressAction {
  ASTextHighlight *highlight = [ASTextHighlight highlightWithBackgroundColor:backgroundColor];
  highlight.userInfo = userInfo;
  highlight.tapAction = tapAction;
  highlight.longPressAction = longPressAction;
  if (color) [self as_setColor:color range:range];
  [self as_setTextHighlight:highlight range:range];
}

- (void)as_setTextHighlightRange:(NSRange)range
                           color:(UIColor *)color
                 backgroundColor:(UIColor *)backgroundColor
                       tapAction:(ASTextAction)tapAction {
  [self as_setTextHighlightRange:range
                           color:color
                 backgroundColor:backgroundColor
                        userInfo:nil
                       tapAction:tapAction
                 longPressAction:nil];
}

- (void)as_setTextHighlightRange:(NSRange)range
                           color:(UIColor *)color
                 backgroundColor:(UIColor *)backgroundColor
                        userInfo:(NSDictionary *)userInfo {
  [self as_setTextHighlightRange:range
                           color:color
                 backgroundColor:backgroundColor
                        userInfo:userInfo
                       tapAction:nil
                 longPressAction:nil];
}

- (void)as_insertString:(NSString *)string atIndex:(NSUInteger)location {
  [self replaceCharactersInRange:NSMakeRange(location, 0) withString:string];
  [self as_removeDiscontinuousAttributesInRange:NSMakeRange(location, string.length)];
}

- (void)as_appendString:(NSString *)string {
  NSUInteger length = self.length;
  [self replaceCharactersInRange:NSMakeRange(length, 0) withString:string];
  [self as_removeDiscontinuousAttributesInRange:NSMakeRange(length, string.length)];
}

- (void)as_removeDiscontinuousAttributesInRange:(NSRange)range {
  NSArray *keys = [NSMutableAttributedString as_allDiscontinuousAttributeKeys];
  for (NSString *key in keys) {
    [self removeAttribute:key range:range];
  }
}

+ (NSArray *)as_allDiscontinuousAttributeKeys {
  static NSArray *keys;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    keys = @[(id)kCTSuperscriptAttributeName,
             (id)kCTRunDelegateAttributeName,
             ASTextBackedStringAttributeName,
             ASTextBindingAttributeName,
             ASTextAttachmentAttributeName,
             (id)kCTRubyAnnotationAttributeName,
             NSAttachmentAttributeName];
  });
  return keys;
}

@end