import UIKit
import PlaygroundSupport

extension NSAttributedString {
    enum MarkdownElement {
        case paragraph, bold
    }

    struct MarkdownStylingOptions {
        var font: UIFont
        var paragraphStyle: NSParagraphStyle = .default

        var boldFont: UIFont {
            let fontDescriptor = font.fontDescriptor.withSymbolicTraits(.traitBold) ?? font.fontDescriptor

            return UIFont(descriptor: fontDescriptor, size: font.pointSize)
        }
    }

    convenience init(
        markdownString: String,
        options: MarkdownStylingOptions,
        applyEffect: ((MarkdownElement, String) -> [NSAttributedString.Key: Any])? = nil
    ) {
        let attributedString = NSMutableAttributedString()

        for paragraph in markdownString.split(separator: "\n\n") {
            let attributedParagraph = NSMutableAttributedString()

            // Replace \n with \u2028 to prevent attributed string from picking up single line breaks as paragraphs.
            let components = paragraph.replacingOccurrences(of: "\n", with: "\u{2028}")
                .components(separatedBy: "**")

            for (index, string) in components.enumerated() {
                var attributes: [NSAttributedString.Key: Any] = [:]

                if index % 2 == 0 {
                    attributes[.font] = options.font
                } else {
                    attributes[.font] = options.boldFont
                    attributes.merge(applyEffect?(.bold, string) ?? [:], uniquingKeysWith: { $1 })
                }

                attributedParagraph.append(NSAttributedString(string: string, attributes: attributes))
            }

            attributedParagraph.addAttributes(
                applyEffect?(.paragraph, attributedParagraph.string) ?? [:],
                range: NSRange(location: 0, length: attributedParagraph.length)
            )

            // Add single line break to form a paragraph.
            attributedParagraph.append(NSAttributedString(string: "\n"))

            attributedString.append(attributedParagraph)
        }

        attributedString.addAttribute(
            .paragraphStyle,
            value: options.paragraphStyle,
            range: NSRange(location: 0, length: attributedString.length)
        )

        self.init(attributedString: attributedString)
    }
}

var paragraphStyle = NSParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle
paragraphStyle.paragraphSpacing = 20

let attributedString = NSAttributedString(
    markdownString: """
Paragraph **A** goes here.
Paragraph **B** goes here.

Paragraph **C** goes here and **that's it**.
""",
    options: .init(
        font: .systemFont(ofSize: 17),
        paragraphStyle: paragraphStyle
    ),
    applyEffect: { element, string in
        print("Apply style to \(element) with source string: \(string)")

        return [:]
    }
)

let textLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 320, height: 480))
textLabel.numberOfLines = 0
textLabel.attributedText = attributedString

PlaygroundPage.current.liveView = textLabel