//
//  Label.swift
//  MVMCoreUI
//
//  Created by Scott Pfeil on 3/22/17.
//  Converted by Christiano, Kevin on 4/2/19.
//  Copyright © 2019 Verizon Wireless. All rights reserved.
//

import MVMCore

public typealias ActionBlock = () -> ()


@objcMembers open class Label: UILabel, MVMCoreViewProtocol, MoleculeViewProtocol, MVMCoreUIViewConstrainingProtocol, MFButtonProtocol {
    //------------------------------------------------------
    // MARK: - Properties
    //------------------------------------------------------
    
    public var makeWholeViewClickable = false
    
    /// Set this property if you want updateView to update the font based on this standard and the size passed in.
    public var standardFontSize: CGFloat = 0.0
    
    /// Set this to use a custom sizing object during updateView instead of the standard.
    public var sizeObject: MFSizeObject?
    public var scaleSize: NSNumber?
    
    /// A specific text index to use as a unique marker.
    public var hero: Int?
    
    // Used for scaling the font in updateView.
    private var originalAttributedString: NSAttributedString?
    
    public var hasText: Bool {
        guard let text = text, let attributedText = attributedText else { return false }
        return !text.isEmpty || !attributedText.string.isEmpty
    }
    
    public var getRange: NSRange {
        NSRange(location: 0, length: text?.count ?? 0)
    }
    
    //------------------------------------------------------
    // MARK: - Multi-Action Text
    //------------------------------------------------------
    
    /// Data store of the tappable ranges of the text.
    public var clauses: [ActionableClause] = [] {
        didSet {
            isUserInteractionEnabled = !clauses.isEmpty
            if clauses.count > 1 {
                clauses.sort { first, second in
                    return first.range.location < second.range.location
                }
            }
        }
    }
    
    /// Used for tappable links in the text.
    public struct ActionableClause {
        public var range: NSRange
        public var actionBlock: ActionBlock
        public var accessibilityID: Int = 0
        
        public func performAction() {
            actionBlock()
        }
        
        public init(range: NSRange, actionBlock: @escaping ActionBlock, accessibilityID: Int = 0) {
            self.range = range
            self.actionBlock = actionBlock
            self.accessibilityID = accessibilityID
        }
    }
    
    //------------------------------------------------------
    // MARK: - Convenience Setter For objective-C
    //------------------------------------------------------
    
    /// Sets the clauses array to empty.
    @objc public func setEmptyClauses() {
        clauses = []
    }
    
    //------------------------------------------------------
    // MARK: - Initialization
    //------------------------------------------------------
    
    @objc public func setupView() {
        backgroundColor = .clear
        numberOfLines = 0
        lineBreakMode = .byWordWrapping
        translatesAutoresizingMaskIntoConstraints = false
        clauses = []
        accessibilityCustomActions = []
        accessibilityTraits = .staticText
        
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(textLinkTapped))
        tapGesture.numberOfTapsRequired = 1
        addGestureRecognizer(tapGesture)
    }
    
    @objc public init() {
        super.init(frame: .zero)
        setupView()
    }
    
    @objc required public init?(coder: NSCoder) {
        super.init(coder: coder)
        setupView()
    }
    
    @objc override public init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }
    
    public init(fontStyle: Styler.Font, _ scale: Bool = true) {
        super.init(frame: .zero)
        setupView()
        
        font = fontStyle.getFont(false)
        textColor = fontStyle.color()
        setScale(scale)
    }
    
    @objc convenience public init(standardFontSize size: CGFloat) {
        self.init()
        standardFontSize = size
    }
    
    /// Convenience to init Label with a link comprised of range, actionMap and delegateObject
    @objc convenience public init(text: String, range: NSRange, actionMap: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) {
        self.init()
        self.text = text
        if let actionBlock = createActionBlockFor(actionMap: actionMap, additionalData: additionalData, delegateObject: delegateObject) {
            setTextLinkState(range: range, actionBlock: actionBlock)
        }
    }
    
    required public init(model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
        super.init(frame: .zero)
        setupView()
        styleB2(true)
        set(with: model, delegateObject, additionalData)
    }
    
    //------------------------------------------------------
    // MARK: - 2.0 Factory Functions
    //------------------------------------------------------
    
    //2.0 fonts init methods
    /// H1 -> HeadlineLarge
    @objc public static func commonLabelH1(_ scale: Bool) -> Label {
        let label = Label.label()
        label.styleH1(scale)
        return label
    }
    
    /// H2 -> Headline
    @objc public static func commonLabelH2(_ scale: Bool) -> Label {
        let label = Label.label()
        label.styleH2(scale)
        return label
    }
    
    /// H3 -> SubHead
    @objc public static func commonLabelH3(_ scale: Bool) -> Label {
        let label = Label.label()
        label.styleH3(scale)
        return label
    }
    
    /// H32 -> Head
    @objc public static func commonLabelH32(_ scale: Bool) -> Label {
        let label = Label.label()
        label.styleH32(scale)
        return label
    }
    
    /// B1 -> SubTitle
    @objc public static func commonLabelB1(_ scale: Bool) -> Label {
        let label = Label.label()
        label.styleB1(scale)
        return label
    }
    
    /// B2 -> Body
    @objc public static func commonLabelB2(_ scale: Bool) -> Label {
        let label = Label.label()
        label.styleB2(scale)
        return label
    }
    
    /// B3 -> Legal
    @objc public static func commonLabelB3(_ scale: Bool) -> Label {
        let label = Label.label()
        label.styleB3(scale)
        return label
    }
    
    /// B20 -> Body
    @objc public static func commonLabelB20(_ scale: Bool) -> Label {
        let label = Label.label()
        label.styleB20(scale)
        return label
    }
    
    /// Default
    @objc open class func label() -> Label {
        Label(frame: .zero)
    }
    
    //------------------------------------------------------
    // MARK: - Style
    //------------------------------------------------------
    
    @objc public static func setLabel(_ label: UILabel?, withHTML html: String?) {
        
        guard let data = html?.data(using: .utf8) else { return }
        
        do {
            label?.attributedText = try NSAttributedString(data: data,
                                                           options: [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.html,
                                                                     NSAttributedString.DocumentReadingOptionKey.characterEncoding: String.Encoding.utf8.rawValue],
                                                           documentAttributes: nil)
        } catch {
            if let coreErrorObject = MVMCoreErrorObject.createErrorObject(for: error, location: "LabelHTMLParse") {
                MVMCoreUILoggingHandler.shared()?.addError(toLog: coreErrorObject)
            }
        }
    }
    
    enum LabelAlignment: String {
        case center
        case right
        case left
    }
    
    public func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject? = nil, _ additionalData: [AnyHashable: Any]? = nil) {
        
        clauses = []
        text = nil
        attributedText = nil
        originalAttributedString = nil
        
        guard let labelModel = model as? LabelModel else { return }
        
        text = labelModel.text
        
        /*
         * This is to address a reuse issue with iOS 13 and up.
         * Even if you set text & attributedText to nil, the moment you set text with a value,
         * attributedText will hold a dirty value from a previously reused cell even if reset() is
         * appropriately called.
         * Only other reference found of issue: https://www.thetopsites.net/article/58142205.shtml
         */
        if #available(iOS 13, *) {
            if let attributedText = attributedText, let text = text, !text.isEmpty {
                let attributedString = NSMutableAttributedString(string: text)
                let range = NSRange(location: 0, length: text.count)
                for attribute in attributedText.attributes(at: 0, effectiveRange: nil) {
                    if attribute.key == .underlineStyle {
                        attributedString.addAttribute(.underlineStyle, value: 0, range: range)
                    }
                    if attribute.key == .strikethroughStyle {
                        attributedString.addAttribute(.strikethroughStyle, value: 0, range: range)
                    }
                }
                
                self.attributedText = attributedString
            }
        }
        
        hero = labelModel.hero
        Label.setLabel(self, withHTML: labelModel.html)
        isAccessibilityElement = hasText
        
        switch labelModel.textAlignment {
        case .center:
            textAlignment = .center
            
        case .right:
            textAlignment = .right
            
        default:
            textAlignment = .left
        }
        
        makeWholeViewClickable = labelModel.makeWholeViewClickable ?? false
        if let backgroundColor = labelModel.backgroundColor {
            self.backgroundColor = backgroundColor.uiColor
        }
        
        if let accessibilityText = labelModel.accessibilityText {
            accessibilityLabel = accessibilityText
        }
        
        if let fontStyle = labelModel.fontStyle?.rawValue {
            MFStyler.styleLabel(self, withStyle: fontStyle, genericScaling: false)
            standardFontSize = font.pointSize
        } else {
            let fontSize = labelModel.fontSize
            if let fontSize = fontSize {
                standardFontSize = fontSize
            }
            if let fontName = labelModel.fontName {
                font = MFFonts.mfFont(withName: fontName, size: fontSize ?? standardFontSize)
            } else if let fontSize = fontSize {
                font = font.updateSize(fontSize)
            }
        }
        
        if let color = labelModel.textColor {
            textColor = color.uiColor
        }
        
        if let lines = labelModel.numberOfLines {
             numberOfLines = lines
        }
            
        if let attributes = labelModel.attributes, let labelText = text {
            let attributedString = NSMutableAttributedString(string: labelText, attributes: [NSAttributedString.Key.font: font.updateSize(standardFontSize), NSAttributedString.Key.foregroundColor: textColor as UIColor])
            
            for attribute in attributes {
                let range = NSRange(location: attribute.location, length: attribute.length)
                switch attribute {
                case let underlineAtt as LabelAttributeUnderlineModel:
                    attributedString.addAttribute(.underlineStyle, value: underlineAtt.underlineValue.rawValue, range: range)
                    if let underlineColor = underlineAtt.color?.uiColor {
                        attributedString.addAttribute(.underlineColor, value: underlineColor, range: range)
                    }
                    
                case _ as LabelAttributeStrikeThroughModel:
                    attributedString.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.thick.rawValue, range: range)
                    attributedString.addAttribute(.baselineOffset, value: 0, range: range)
                    
                case let colorAtt as LabelAttributeColorModel:
                    if let colorHex = colorAtt.textColor {
                        attributedString.removeAttribute(.foregroundColor, range: range)
                        attributedString.addAttribute(.foregroundColor, value: colorHex.uiColor, range: range)
                    }
                    
                case let imageAtt as LabelAttributeImageModel:
                    var fontSize = font.pointSize
                    if let attributeSize = imageAtt.size {
                        fontSize = attributeSize
                    }
                    let imageName = imageAtt.name ?? "externalLink"
                    let imageAttachment: NSTextAttachment
                    
                    if let url = imageAtt.URL {
                        imageAttachment = Label.getTextAttachmentFrom(url: url, dimension: fontSize, label: self)
                    } else {
                        imageAttachment = Label.getTextAttachmentImage(name: imageName, dimension: fontSize)
                    }
                    
                    // Confirm that the intended image location is within range.
                    if 0...labelText.count ~= imageAtt.location {
                        let mutableString = NSMutableAttributedString()
                        mutableString.append(NSAttributedString(attachment: imageAttachment))
                        attributedString.insert(mutableString, at: imageAtt.location)
                    }
                    
                case let fontAtt as LabelAttributeFontModel:
                    if let fontStyle = fontAtt.style {
                        attributedString.removeAttribute(.font, range: range)
                        attributedString.removeAttribute(.foregroundColor, range: range)
                        attributedString.addAttribute(.font, value: fontStyle.getFont(), range: range)
                        attributedString.addAttribute(.foregroundColor, value: fontStyle.color(), range: range)
                    } else {
                        let fontSize = fontAtt.size
                        var font: UIFont?
                        
                        if let fontName = fontAtt.name {
                            font = MFFonts.mfFont(withName: fontName, size: fontSize ?? self.font.pointSize)
                        } else if let fontSize = fontSize {
                            font = self.font.updateSize(fontSize)
                        }
                        if let font = font {
                            attributedString.removeAttribute(.font, range: range)
                            attributedString.addAttribute(.font, value: font, range: range)
                        }
                    }
                case let actionAtt as LabelAttributeActionModel:
                    addTappableLinkAttribute(range: NSRange(location: range.location, length: range.length)) {
                        MVMCoreActionHandler.shared()?.asyncHandleAction(with: actionAtt.action, additionalData: additionalData, delegateObject: delegateObject)
                    }
                    addActionAttributes(range: range, string: attributedString)
                    
                default:
                    continue
                }
            }
            
            attributedText = attributedString
            originalAttributedString = attributedText
        }
    }
    
    @objc public static func setUILabel(_ label: UILabel?, withJSON json: [AnyHashable: Any]?, delegate: DelegateObject?, additionalData: [AnyHashable: Any]?) {
        guard let label = label else { return }
        
        // Some properties can only be set on Label.
        // Label fonts should not be scaled because it will be scaled in updateView.
        let mvmLabel = label as? Label
        
        label.text = json?.optionalStringForKey(KeyText)
        
        setLabel(label, withHTML: json?.optionalStringForKey("html"))
        
        if let alignment = json?.optionalStringForKey("textAlignment") {
            switch alignment {
            case "center":
                label.textAlignment = .center
            case "right":
                label.textAlignment = .right
            default:
                label.textAlignment = .left
            }
        }
        
        mvmLabel?.makeWholeViewClickable = json?.boolForKey("makeWholeViewClickable") ?? false
        
        if let backgroundColorHex = json?.optionalStringForKey(KeyBackgroundColor), !backgroundColorHex.isEmpty {
            label.backgroundColor = UIColor.mfGet(forHex: backgroundColorHex)
        }
        
        label.accessibilityLabel = json?.optionalStringForKey("accessibilityText")
        
        if let fontStyle = json?.optionalStringForKey("fontStyle") {
            MFStyler.styleLabel(label, withStyle: fontStyle, genericScaling: mvmLabel == nil)
            mvmLabel?.standardFontSize = label.font.pointSize
        } else {
            let fontSize = json?["fontSize"] as? CGFloat
            if let fontSize = fontSize {
                mvmLabel?.standardFontSize = fontSize
            }
            
            if let fontName = json?.optionalStringForKey("fontName") {
                label.font = MFFonts.mfFont(withName: fontName, size: fontSize ?? mvmLabel?.standardFontSize ?? label.font.pointSize)
            } else if let fontSize = fontSize {
                label.font = label.font.updateSize(fontSize)
            }
        }
        
        if let textColorHex = json?.optionalStringForKey(KeyTextColor), !textColorHex.isEmpty {
            label.textColor = UIColor.mfGet(forHex: textColorHex)
        }
        
        if let attributes = json?.optionalArrayForKey("attributes"), let labelText = label.text {
            let attributedString = NSMutableAttributedString(string: labelText,
                                                             attributes: [NSAttributedString.Key.font: mvmLabel?.font.updateSize(mvmLabel!.standardFontSize) ?? label.font as UIFont,
                                                                          NSAttributedString.Key.foregroundColor: label.textColor as UIColor])
            for case let attribute as [String: Any] in attributes {
                guard let attributeType = attribute.optionalStringForKey(KeyType),
                    let location = attribute["location"] as? Int,
                    let length = attribute["length"] as? Int
                    else { continue }
                
                let range = NSRange(location: location, length: length)
                
                switch attributeType {
                case "underline":
                    attributedString.addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: range)
                    
                case "strikethrough":
                    attributedString.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.thick.rawValue, range: range)
                    attributedString.addAttribute(.baselineOffset, value: 0, range: range)
                    
                case "color":
                    if let colorHex = attribute.optionalStringForKey(KeyTextColor), !colorHex.isEmpty {
                        attributedString.removeAttribute(.foregroundColor, range: range)
                        attributedString.addAttribute(.foregroundColor, value: UIColor.mfGet(forHex: colorHex), range: range)
                    }
                case "image":
                    let fontSize = attribute["size"] as? CGFloat ?? label.font.pointSize
                    let imageName = attribute["name"] as? String ?? "externalLink"
                    let imageURL = attribute["URL"] as? String
                    let imageAttachment: NSTextAttachment
                    
                    if let url = imageURL, let label = label as? Label {
                        imageAttachment = Label.getTextAttachmentFrom(url: url, dimension: fontSize, label: label)
                    } else {
                        imageAttachment = Label.getTextAttachmentImage(name: imageName, dimension: fontSize)
                    }
                    
                    let mutableString = NSMutableAttributedString()
                    mutableString.append(NSAttributedString(attachment: imageAttachment))
                    attributedString.insert(mutableString, at: location)
                    
                case "font":
                    if let fontStyle = attribute.optionalStringForKey("style") {
                        let styles = MFStyler.styleGetAttributedString("0", withStyle: fontStyle, genericScaling: mvmLabel == nil)
                        attributedString.removeAttribute(.font, range: range)
                        attributedString.removeAttribute(.foregroundColor, range: range)
                        attributedString.addAttributes(styles.attributes(at: 0, effectiveRange: nil), range: range)
                    } else {
                        let fontSize = attribute["size"] as? CGFloat
                        var font: UIFont?
                        
                        if let fontName = attribute.optionalStringForKey("name") {
                            font = MFFonts.mfFont(withName: fontName, size: fontSize ?? mvmLabel?.standardFontSize ?? label.font.pointSize)
                        } else if let fontSize = fontSize {
                            font = label.font.updateSize(fontSize)
                        }
                        
                        if let font = font {
                            attributedString.removeAttribute(.font, range: range)
                            attributedString.addAttribute(.font, value: font, range: range)
                        }
                    }
                case "action":
                    guard let actionLabel = label as? Label else { continue }
                    
                    actionLabel.addActionAttributes(range: range, string: attributedString)
                    if let actionBlock = actionLabel.createActionBlockFor(actionMap: attribute, additionalData: additionalData, delegateObject: delegate) {
                        actionLabel.appendActionableClause(range: range, actionBlock: actionBlock)
                    }
                    
                default:
                    continue
                }
            }
            label.attributedText = attributedString
            mvmLabel?.originalAttributedString = attributedString
        }
    }
    
    //------------------------------------------------------
    // MARK: - 3.0 Styling Method
    //------------------------------------------------------
    
    public func setFontStyle(_ fontStyle: Styler.Font, _ scale: Bool = true) {
        font = fontStyle.getFont(false)
        textColor = .mvmBlack
        setScale(scale)
    }
    
    //------------------------------------------------------
    // MARK: - 2.0 Styling Methods
    //------------------------------------------------------
    
    @objc public func styleH1(_ scale: Bool) {
        MFStyler.styleLabelH1(self, genericScaling: false)
        setScale(scale)
    }
    
    @objc public func styleH2(_ scale: Bool) {
        MFStyler.styleLabelH2(self, genericScaling: false)
        setScale(scale)
    }
    
    @objc public func styleH3(_ scale: Bool) {
        MFStyler.styleLabelH3(self, genericScaling: false)
        setScale(scale)
    }
    
    @objc public func styleH32(_ scale: Bool) {
        MFStyler.styleLabelH32(self, genericScaling: false)
        setScale(scale)
    }
    
    @objc public func styleB1(_ scale: Bool) {
        MFStyler.styleLabelB1(self, genericScaling: false)
        setScale(scale)
    }
    
    @objc public func styleB2(_ scale: Bool) {
        MFStyler.styleLabelB2(self, genericScaling: false)
        setScale(scale)
    }
    
    @objc public func styleB3(_ scale: Bool) {
        MFStyler.styleLabelB3(self, genericScaling: false)
        setScale(scale)
    }
    
    @objc public func styleB20(_ scale: Bool) {
        MFStyler.styleLabelB20(self, genericScaling: false)
        setScale(scale)
    }
    
    /// Will remove the values contained in attributedText.
    func clearAttributes() {
        guard let labelText = text, !labelText.isEmpty else { return }
        guard let attributes = attributedText?.attributes(at: 0, longestEffectiveRange: nil, in: NSRange(location: 0, length: labelText.count))
            else { return }
        
        let attributedString = NSMutableAttributedString(string: labelText)
        
        for attribute in attributes {
            attributedString.removeAttribute(attribute.key, range: NSRange(location: 0, length: labelText.count))
        }
        
        attributedText = attributedString
    }

    
    @objc public func updateView(_ size: CGFloat) {
        scaleSize = size as NSNumber
        
        if let originalAttributedString = originalAttributedString {
            let attributedString = NSMutableAttributedString(attributedString: originalAttributedString)
            attributedString.removeAttribute(.font, range: NSRange(location: 0, length: attributedString.length))
            
            // Loop the original attributed string, resize the fonts.
            originalAttributedString.enumerateAttribute(.font, in: NSRange(location: 0, length: originalAttributedString.length), options: []) { value, range, stop in
                
                if let fontObj = value as? UIFont, let stylerSize = MFStyler.sizeObjectGeneric(forCurrentDevice: fontObj.pointSize)?.getValueBased(onSize: size) {
                    attributedString.addAttribute(.font, value: fontObj.updateSize(stylerSize) as Any, range: range)
                }
            }
            
            // Loop the original attributed string, resize the image attachments.
            originalAttributedString.enumerateAttribute(.attachment, in: NSRange(location: 0, length: originalAttributedString.length), options: []) { value, range, stop in
                if let attachment = value as? NSTextAttachment,
                    let stylerSize = MFStyler.sizeObjectGeneric(forCurrentDevice: attachment.bounds.width)?.getValueBased(onSize: size) {
                    
                    let dimension = round(stylerSize)
                    attachment.bounds = CGRect(x: 0, y: 0, width: dimension, height: dimension)
                }
            }
            
            attributedText = attributedString
        } else if !MVMCoreGetterUtility.fequal(a: Float(standardFontSize), b: 0.0), let sizeObject = sizeObject ?? MFStyler.sizeObjectGeneric(forCurrentDevice: standardFontSize) {
            font = font.updateSize(sizeObject.getValueBased(onSize: size))
        }
    }

    @objc public func setFont(_ font: UIFont, scale: Bool) {
        self.font = font
        setScale(scale)
    }
    
    @objc public func setScale(_ scale: Bool) {
        if scale {
            standardFontSize = font.pointSize
            if let floatScale = scaleSize?.floatValue {
                updateView(CGFloat(floatScale))
            } else {
                updateView(MVMCoreUIUtility.getWidth())
            }
        } else {
            standardFontSize = 0
        }
    }
    
    /**
     Appends an external link image to the end of the attributed string.
     Will provide one whitespace to the left of the icon; adds 2 chars to the end of the string.
     */
    @objc public func appendExternalLinkIcon() {
        
        guard let attributedText = attributedText else { return }
        
        let mutableString = NSMutableAttributedString(attributedString: attributedText)
        
        mutableString.append(NSAttributedString(string: " "))
        mutableString.append(NSAttributedString(attachment: Label.getTextAttachmentImage(dimension: font.pointSize)))
        self.attributedText = mutableString
    }
    
    /**
     Insert external link icon anywhere within text of Label.
     
     - Note: Each icon insertion adds 1 additional characters to the overall text length.
     Therefore, you **MUST** insert icons and links in the order they would appear.
     - parameter index: Location within the associated text to insert an external Link Icon
     */
    public func insertExternalLinkIcon(at index: Int) {
        
        guard let attributedText = attributedText, index <= attributedText.string.count && index >= 0 else { return }
        
        let mutableString = NSMutableAttributedString(attributedString: attributedText)
        mutableString.insert(NSAttributedString(attachment: Label.getTextAttachmentImage(dimension: font.pointSize)), at: index)
        
        self.attributedText = mutableString
    }
    
    /*
     Retrieves an NSTextAttachment for NSAttributedString that is prepped to be inserted with the text.
     
     - parameter name: The Asset name of the image. DEFAULT: "externalLink"
     - parameter dimension: length of the height and width of the image. Will be 80% the passed magnitude.
     */
    static func getTextAttachmentImage(name: String = "externalLink", dimension: CGFloat) -> NSTextAttachment {
        
        let imageAttachment = NSTextAttachment()
        imageAttachment.image = MVMCoreCache.shared()?.getImageFromRegisteredBundles(name)
        imageAttachment.bounds = CGRect(x: 0, y: 0, width: dimension, height: dimension)
        
        return imageAttachment
    }
    
    static func getTextAttachmentFrom(url: String, dimension: CGFloat, label: Label) -> NSTextAttachment {
        
        let imageAttachment = NSTextAttachment()
        imageAttachment.bounds = CGRect(x: 0, y: 0, width: dimension, height: dimension)
        
        DispatchQueue.global(qos: .default).async {
            MVMCoreCache.shared()?.getImage(url, useWidth: false, widthForS7: 0, useHeight: false, heightForS7: 0, localFallbackImageName: nil) { image, data, _ in
                
                DispatchQueue.main.sync {
                    imageAttachment.image = image
                    label.setNeedsDisplay()
                }
            }
        }
        return imageAttachment
    }
    
    /// Call to detect in the attributedText contains an NSTextAttachment.
    func containsTextAttachment() -> Bool {
        
        guard let attributedText = attributedText else { return false }
        
        var containsAttachment = false
        
        attributedText.enumerateAttribute(.attachment, in: NSRange(location: 0, length: attributedText.length), options: []) { value, range, stop in
            if value is NSTextAttachment {
                containsAttachment = true
                return
            }
        }
        
        return containsAttachment
    }
    
    func appendActionableClause(range: NSRange, actionBlock: @escaping ActionBlock) {
        
        accessibilityTraits = .button
        let accessibleAction = customAccessibilityAction(range: range)
        clauses.append(ActionableClause(range: range, actionBlock: actionBlock, accessibilityID: accessibleAction?.hash ?? -1))
    }
    
    /**
     Provides a text container and layout manager of how the text would appear on screen.
     They are used in tandem to derive low-level TextKit results of the label.
     */
    public func abstractTextContainer() -> (NSTextContainer, NSLayoutManager, NSTextStorage)? {
        
        // Must configure the attributed string to translate what would appear on screen to accurately analyze.
        guard let attributedText = attributedText else { return nil }
        
        let paragraph = NSMutableParagraphStyle()
        paragraph.alignment = textAlignment
        
        let stagedAttributedString = NSMutableAttributedString(attributedString: attributedText)
        stagedAttributedString.addAttributes([NSAttributedString.Key.paragraphStyle: paragraph], range: NSRange(location: 0, length: attributedText.string.count))
        
        let textStorage = NSTextStorage(attributedString: stagedAttributedString)
        let layoutManager = NSLayoutManager()
        let textContainer = NSTextContainer(size: .zero)
        
        layoutManager.addTextContainer(textContainer)
        textStorage.addLayoutManager(layoutManager)
        
        textContainer.lineFragmentPadding = 0.0
        textContainer.lineBreakMode = lineBreakMode
        textContainer.maximumNumberOfLines = numberOfLines
        textContainer.size = bounds.size
        
        return (textContainer, layoutManager, textStorage)
    }
    
    public static func boundingRect(forCharacterRange range: NSRange, in label: Label) -> CGRect {
        
        guard let abstractContainer = label.abstractTextContainer() else { return CGRect() }
        let textContainer = abstractContainer.0
        let layoutManager = abstractContainer.1
        
        var glyphRange = NSRange()
        
        // Convert the range for glyphs.
        layoutManager.characterRange(forGlyphRange: range, actualGlyphRange: &glyphRange)
        
        return layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
    }
}

// MARK: - Atomization
extension Label {
    
    public func reset() {
        text = nil
        attributedText = nil
        hero = nil
        textAlignment = .left
        originalAttributedString = nil
        styleB2(true)
        accessibilityCustomActions = []
        clauses = []
        accessibilityTraits = .staticText
        numberOfLines = 0
    }
    
    public func needsToBeConstrained() -> Bool { true }
    
    public func horizontalAlignment() -> UIStackView.Alignment { .leading }
    
    public func copyBackgroundColor() -> Bool { true }
}

// MARK: - Multi-Link Functionality
extension Label {
    
    /// Applied to existing text. Removes underlines of tappable links and assoated actionable clauses.
    @objc public func clearActionableClauses() {
        guard let attributedText = attributedText else { return }
        let mutableAttributedString = NSMutableAttributedString(attributedString: attributedText)
        
        clauses.forEach { clause in
            mutableAttributedString.removeAttribute(NSAttributedString.Key.underlineStyle, range: clause.range)
        }
        
        self.attributedText = mutableAttributedString
        accessibilityElements = []
        clauses = []
    }
    
    public func createActionBlockFor(actionMap: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) -> ActionBlock? {
        return { [weak self] in
            guard let self = self else { return }
            
            if (delegateObject as? MVMCoreUIDelegateObject)?.buttonDelegate?.button?(self, shouldPerformActionWithMap: actionMap, additionalData: additionalData) ?? true {
                MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: additionalData, delegateObject: delegateObject)
            }
        }
    }
    
    private func addActionAttributes(range: NSRange, string: NSMutableAttributedString?) {
        
        guard let string = string else { return }
        
        string.addAttributes([NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue], range: range)
    }
    
    fileprivate func setActionAttributes(range: NSRange) {
        
        guard let attributedText = attributedText else { return }
        
        let mutableAttributedString = NSMutableAttributedString(attributedString: attributedText)
        addActionAttributes(range: range, string: mutableAttributedString)
        self.attributedText = mutableAttributedString
    }
    
    /**
     Provides an actionable range of text.
     
     - Attention: This method expects text to be set first. Otherwise, it will do nothing.
     - parameter range: The range of text to be tapped.
     - parameter actionBlock: The code triggered when tapping the range of text.
     */
    @objc public func addTappableLinkAttribute(range: NSRange, actionBlock: @escaping ActionBlock) {
        
        setTextLinkState(range: range, actionBlock: actionBlock)
    }
    
    /**
     Provides an actionable range of text.
     
     - Attention: This method expects text to be set first. Otherwise, it will do nothing.
     - parameter range: The range of text to be tapped.
     */
    @objc public func addTappableLinkAttribute(range: NSRange, actionMap: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) {
        
        if let actionBlock = createActionBlockFor(actionMap: actionMap, additionalData: additionalData, delegateObject: delegateObject) {
            setTextLinkState(range: range, actionBlock: actionBlock)
        }
    }
    
    /// Converts the entire text into a link. All characters will be underlined and the intrinsic bounds will respond to tap.
    @objc public func makeTextButton(actionMap: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) {
        
        if let actionBlock = createActionBlockFor(actionMap: actionMap, additionalData: additionalData, delegateObject: delegateObject) {
            setTextLinkState(range: getRange, actionBlock: actionBlock)
        }
    }
    
    /// Converts the entire text into a link. All characters will be underlined and the intrinsic bounds will respond to tap.
    @objc public func makeTextButton(actionBlock: @escaping ActionBlock) {
        
        setTextLinkState(range: getRange, actionBlock: actionBlock)
    }
    
    /// Underlines the tappable region and stores the tap logic for interation.
    private func setTextLinkState(range: NSRange, actionBlock: @escaping ActionBlock) {
        
        setActionAttributes(range: range)
        appendActionableClause(range: range, actionBlock: actionBlock)
    }
    
    @objc private func textLinkTapped(_ gesture: UITapGestureRecognizer) {
        
        for clause in clauses {
            // This determines if we tapped on the desired range of text.
            if gesture.didTapAttributedTextInLabel(self, inRange: clause.range) {
                clause.performAction()
                return
            }
        }
    }
}

// MARK: -
extension UITapGestureRecognizer {
    
    func didTapAttributedTextInLabel(_ label: Label, inRange targetRange: NSRange) -> Bool {
        
        // There would only ever be one clause to act on.
        if label.makeWholeViewClickable {
            return true
        }
        
        guard let abstractContainer = label.abstractTextContainer() else { return false }
        let textContainer = abstractContainer.0
        let layoutManager = abstractContainer.1
        
        let tapLocation = location(in: label)
        let indexOfGlyph = layoutManager.glyphIndex(for: tapLocation, in: textContainer)
        let intrinsicWidth = label.intrinsicContentSize.width
        
        // Assert that tapped occured within acceptable bounds based on alignment.
        switch label.textAlignment {
        case .right:
            if tapLocation.x < label.bounds.width - intrinsicWidth {
                return false
            }
        case .center:
            let halfBounds = label.bounds.width / 2
            let halfIntrinsicWidth = intrinsicWidth / 2
            
            if tapLocation.x > halfBounds + halfIntrinsicWidth {
                return false
            } else if tapLocation.x < halfBounds - halfIntrinsicWidth {
                return false
            }
        default: // Left align
            if tapLocation.x > intrinsicWidth {
                return false
            }
        }
        
        // Affirms that the tap occured in the desired rect of provided by the target range.
        return layoutManager.boundingRect(forGlyphRange: targetRange, in: textContainer).contains(tapLocation) && NSLocationInRange(indexOfGlyph, targetRange)
    }
}

// MARK: - Accessibility
extension Label {
    
    func customAccessibilityAction(range: NSRange) -> UIAccessibilityCustomAction? {
        
        guard let text = text else { return nil }
        
        if accessibilityHint == nil {
            accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: "swipe_to_select_with_action_hint")
        }
        
        let actionText = NSString(string: text).substring(with: range)
        let accessibleAction = UIAccessibilityCustomAction(name: actionText, target: self, selector: #selector(accessibilityCustomAction(_:)))
        accessibilityCustomActions?.append(accessibleAction)
        
        return accessibleAction
    }
    
    @objc public func accessibilityCustomAction(_ action: UIAccessibilityCustomAction) {
        
        for clause in clauses {
            if action.hash == clause.accessibilityID {
                clause.performAction()
                return
            }
        }
    }
    
    open override func accessibilityActivate() -> Bool {
        
        guard let accessibleActions = accessibilityCustomActions else { return false }
        
        for clause in clauses {
            for action in accessibleActions {
                if action.hash == clause.accessibilityID {
                    clause.performAction()
                    return true
                }
            }
        }
        
        return false
    }
}
