//
//  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
import VDS

public typealias ActionBlock = () -> ()


@objcMembers open class Label: VDS.Label, VDSMoleculeViewProtocol, MVMCoreUIViewConstrainingProtocol, MFButtonProtocol, ViewMaskingProtocol {
    //------------------------------------------------------
    // MARK: - Properties
    //------------------------------------------------------
    open var viewModel: LabelModel!
    open var delegateObject: MVMCoreUIDelegateObject?
    open var additionalData: [AnyHashable : Any]?
    
    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.
    @available(*, deprecated, message: "VDS is maintaining scaleSize")
    public var sizeObject: MFSizeObject?
    @available(*, deprecated, message: "VDS is maintaining scaleSize")
    public var scaleSize: NSNumber?
    
    /// A specific text index to use as a unique marker.
    public var hero: Int?
    
    public var getRange: NSRange {
        NSRange(location: 0, length: text?.count ?? 0)
    }
    
    public var shouldMaskWhileRecording: Bool = false
    
    public var hasText: Bool {
        guard let text = text, let attributedText = attributedText else { return false }
        return !text.isEmpty || !attributedText.string.isEmpty
    }
    
    //------------------------------------------------------
    // MARK: - Convenience Setter For objective-C
    //------------------------------------------------------
    
    /// Sets the clauses array to empty.
    @objc public func setEmptyClauses() {
    }
    
    //------------------------------------------------------
    // MARK: - Initialization
    //------------------------------------------------------
    
    @objc public required init() {
        super.init()
    }
    
    @objc required public init?(coder: NSCoder) {
        super.init(coder: coder)
    }
    
    @objc override public init(frame: CGRect) {
        super.init(frame: frame)
    }
    
    public init(fontStyle: Styler.Font, _ scale: Bool = true) {
        super.init(frame: .zero)
        setFontStyle(fontStyle, 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)
        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)
            }
        }
    }
    
    @objc public func resetAttributeStyle() {
        setNeedsUpdate()
    }
    
    public func viewModelDidUpdate() {
        shouldMaskWhileRecording = viewModel.shouldMaskRecordedView ?? false
        text = viewModel.text
        hero = viewModel.hero
        Label.setLabel(self, withHTML: viewModel.html)
        textAlignment = viewModel.textAlignment ?? .left
        surface = viewModel.surface
        
        makeWholeViewClickable = viewModel.makeWholeViewClickable ?? false
        if let backgroundColor = viewModel.backgroundColor {
            self.backgroundColor = backgroundColor.uiColor
        }
        
        if let style = viewModel.fontStyle?.vdsTextStyle() {
            textStyle = style
        } else if let fontName = viewModel.fontName {
            // there is a TextStyle.defaultStyle
            let fontSize = viewModel.fontSize
            if let fontSize {
                standardFontSize = fontSize
            }
            if let newFont = UIFont(name: fontName, size: fontSize ?? standardFontSize) {
                font = newFont
            }
        } else if let fontSize = viewModel.fontSize {
            standardFontSize = fontSize
            font = textStyle.font.withSize(fontSize)
        }
        
        if let color = viewModel.textColor {
            textColorConfiguration = SurfaceColorConfiguration(color.uiColor, color.uiColor).eraseToAnyColorable()
        }
        
        if let lines = viewModel.numberOfLines {
            numberOfLines = lines
        }
        
        if let attributeModels = viewModel.attributes?.toVDSLabelAttributeModel(delegateObject: delegateObject, additionalData: additionalData) {
            attributes = attributeModels
        }
        
    }
    
    open override func updateAccessibility() {
        super.updateAccessibility()
        if let accessibilityTraits = viewModel?.accessibilityTraits {
            self.accessibilityTraits = accessibilityTraits
        }
        
        if let accessibilityText = viewModel?.accessibilityText {
            accessibilityLabel = accessibilityText
        }
    }
    
    @objc open override func reset() {
        super.reset()
    }
    
    @objc open func updateView(_ size: CGFloat) { }
    
    @objc open func setFont(_ font: UIFont, scale: Bool) {
        self.font = font
        setScale(scale)
    }

    @objc open func setScale(_ scale: Bool) {
        if scale {
            standardFontSize = font.pointSize
        } else {
            standardFontSize = 0
        }
    }
    
    @objc public static func setUILabel(_ label: UILabel?, withJSON json: [AnyHashable: Any]?, delegate: DelegateObject?, additionalData: [AnyHashable: Any]?) {
        guard let label = label as? Label,
              let json = json as? [String: Any],
              let labelModel = try? LabelModel.decode(jsonDict: json) else { return }
        label.set(with: labelModel, delegate as? MVMCoreUIDelegateObject, additionalData)
    }
    
    //------------------------------------------------------
    // MARK: - 3.0 Styling Method
    //------------------------------------------------------
    
    public func setFontStyle(_ fontStyle: Styler.Font, _ scale: Bool = true) {
        guard let style = fontStyle.vdsTextStyle() else { return }
        textStyle = style
        setScale(scale)
    }
    
    //------------------------------------------------------
    // MARK: - 2.0 Styling Methods
    //------------------------------------------------------
    @objc public func styleH1(_ scale: Bool) {
        setFontStyle(.H1, scale)
    }
    
    @objc public func styleH2(_ scale: Bool) {
        setFontStyle(.H2, scale)
    }
    
    @objc public func styleH3(_ scale: Bool) {
        setFontStyle(.H3, scale)
    }
    
    @objc public func styleH32(_ scale: Bool) {
        setFontStyle(.H32, scale)
    }
    
    @objc public func styleB1(_ scale: Bool) {
        setFontStyle(.B1, scale)
    }
    
    @objc public func styleB2(_ scale: Bool) {
        setFontStyle(.B2, scale)
    }
    
    @objc public func styleB3(_ scale: Bool) {
        setFontStyle(.B3, scale)
    }
    
    @objc public func styleB20(_ scale: Bool) {
        setFontStyle(.B20, scale)
    }
    
}

// Mark: - Old Helpers
extension Label {
    /**
     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
    }
    
    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)
    }
    
    /**
     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)
    }
}

// MARK: - Atomization
extension Label {
        
    public func needsToBeConstrained() -> Bool { true }
    
    public func horizontalAlignment() -> UIStackView.Alignment { .leading }
    
    public func copyBackgroundColor() -> Bool { true }
}

// MARK: - Multi-Link Functionality
extension Label {

    /// Underlines the tappable region and stores the tap logic for interation.
    private func setTextLinkState(range: NSRange, actionBlock: @escaping ActionBlock) {
        guard let text, text.isValid(range: range) else { return }

        var textLink = ActionLabelAttribute(location: range.location, length: range.length)
        textLink.subscriber = textLink
            .action
            .sink { _ in
                actionBlock()
            }
        if var attributes {
            attributes.append(textLink)
            setNeedsUpdate()
        } else {
            attributes = [textLink]
        }
    }
    
    @objc public func clearActionableClauses() {
        attributes = attributes?.filter { !($0 is (any ActionLabelAttributeModel)) }
    }
    
    public func createActionBlockFor(actionMap: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) -> ActionBlock? {
        guard let actionMap else { return nil }
        
        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)
            }
        }
    }
    
    /**
     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)
    }
}

//------------------------------------------------------
// MARK: - Validations
//------------------------------------------------------

func validateAttribute(range: NSRange, in string: NSAttributedString, type: String = "") -> NSRange? {
    guard range.location >= 0 && range.location <= string.length else {
        MVMCoreLoggingHandler.shared()?.addError(toLog: MVMCoreErrorObject(title: nil, messageToLog: "Attribute starting location \(range.lowerBound) is out of bounds for '\(string.string)'. Attribute is discarded.", code: ErrorCode.default.rawValue, domain: ErrorDomainNative, location: "\(#file): \(#function)")!)
        return nil
    }
    
    if type != "image" && range.upperBound > string.length {
        let newRange = NSRange(location: range.location, length: string.length - range.location)
        MVMCoreLoggingHandler.shared()?.addError(toLog: MVMCoreErrorObject(title: nil, messageToLog: "Attribute ending location \(range.upperBound) is out of bounds for '\(string)'. Adjusting to \(newRange.upperBound).", code: ErrorCode.default.rawValue, domain: ErrorDomainNative, location: "\(#file): \(#function)")!)
        return newRange
    }
    
    return range
}

