//
//  BaseEmojiLabel.swift
//  Tusker
//
//  Created by Shadowfacts on 10/18/20.
//  Copyright © 2020 Shadowfacts. All rights reserved.
//

import UIKit
import Pachyderm
import WebURLFoundationExtras

private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])

protocol BaseEmojiLabel: AnyObject {
    var emojiIdentifier: AnyHashable? { get set }
    var emojiRequests: [ImageCache.Request] { get set }
    var emojiFont: UIFont { get }
    var emojiTextColor: UIColor { get }
}

extension BaseEmojiLabel {
    func replaceEmojis<ID: Hashable>(in attributedString: NSAttributedString, emojis: [Emoji], identifier: ID?, setAttributedString: @escaping (_ attributedString: NSAttributedString, _ didReplaceEmojis: Bool) -> Void) {
        // blergh
        precondition(Thread.isMainThread)
        
        let identifier = AnyHashable(identifier)
        emojiIdentifier = identifier
        emojiRequests.forEach { $0.cancel() }
        emojiRequests = []
        
        guard !emojis.isEmpty else {
            setAttributedString(attributedString, false)
            return
        }
        
        let matches = emojiRegex.matches(in: attributedString.string, options: [], range: NSRange(location: 0, length: attributedString.length))
        guard !matches.isEmpty else {
            setAttributedString(attributedString, false)
            return
        }
        
        // Based on https://github.com/ReticentJohn/Amaroq/blob/7c5b7088eb9fd1611dcb0f47d43bf8df093e142c/DireFloof/InlineImageHelpers.m
        let adjustedCapHeight = emojiFont.capHeight - 1
        func emojiImageSize(_ image: UIImage) -> CGSize {
            var imageSizeMatchingFontSize = CGSize(width: image.size.width * (adjustedCapHeight / image.size.height), height: adjustedCapHeight)
            var scale: CGFloat = 1.4
            scale *= UIScreen.main.scale
            imageSizeMatchingFontSize = CGSize(width: imageSizeMatchingFontSize.width * scale, height: imageSizeMatchingFontSize.height * scale)
            return imageSizeMatchingFontSize
        }
        
        let emojiImages = MultiThreadDictionary<String, UIImage>()
        var foundEmojis = false
        
        let group = DispatchGroup()
        
        for emoji in emojis {
            // only make requests for emojis that are present in the text to avoid making unnecessary network requests
            guard matches.contains(where: { (match) -> Bool in
                let matchShortcode = (attributedString.string as NSString).substring(with: match.range(at: 1))
                return emoji.shortcode == matchShortcode
            }) else {
                continue
            }
            
            foundEmojis = true
            
            if let image = ImageCache.emojis.get(URL(emoji.url)!)?.image {
                // if the image is cached, add it immediately.
                // we generate the thumbnail on the main thread, because it's usually fast enough
                // and the delay caused by doing it asynchronously looks works.
                // todo: consider caching these somewhere? the cache needs to take into account both the url and the font size, which may vary across labels, so can't just use ImageCache
                if let thumbnail = image.preparingThumbnail(of: emojiImageSize(image)),
                   let cgImage = thumbnail.cgImage {
                    // the thumbnail API takes a pixel size and returns an image with scale 1, but we want the actual screen scale, so convert
                    // see FB12187798
                    emojiImages[emoji.shortcode] = UIImage(cgImage: cgImage, scale: UIScreen.main.scale, orientation: .up)
                }
            } else {
                // otherwise, perform the network request
                
                group.enter()
                let request = ImageCache.emojis.getFromSource(URL(emoji.url)!) { (_, image) in
                    guard let image else {
                        group.leave()
                        return
                    }
                    image.prepareThumbnail(of: emojiImageSize(image)) { thumbnail in
                        guard let thumbnail = thumbnail?.cgImage,
                              case let rescaled = UIImage(cgImage: thumbnail, scale: UIScreen.main.scale, orientation: .up),
                              let transformedImage = ImageGrayscalifier.convertIfNecessary(url: URL(emoji.url)!, image: rescaled) else {
                            group.leave()
                            return
                        }
                        emojiImages[emoji.shortcode] = transformedImage
                        group.leave()
                    }
                }
                if let request = request {
                    emojiRequests.append(request)
                }
            }
        }
        
        guard foundEmojis else {
            setAttributedString(attributedString, false)
            return
        }
        
        func buildStringWithEmojisReplaced(usePlaceholders: Bool) -> NSAttributedString {
            let mutAttrString = NSMutableAttributedString(attributedString: attributedString)
            
            // OSAllocatedUnfairLock.withLock expects a @Sendable closure, so this warns about captures of non-sendable types (attribute dstrings, text checking results)
            // even though the closures is invoked on the same thread that withLock is called, so it's unclear why it needs to be @Sendable (FB11494878)
            // so, just ignore the warnings
            let emojiAttachments = emojiImages.withLock {
                return $0.mapValues { image in
                    NSTextAttachment(image: image)
                }
            }
            let placeholder = usePlaceholders ? NSTextAttachment(emojiPlaceholderIn: self.emojiFont) : nil
            
            // replaces the emojis starting from the end of the string as to not alter the indices of preceeding emojis
            for match in matches.reversed() {
                let shortcode = (attributedString.string as NSString).substring(with: match.range(at: 1))
                let attachment: NSTextAttachment
                if let emoji = emojiAttachments[shortcode] {
                    attachment = emoji
                } else if usePlaceholders {
                    attachment = placeholder!
                } else {
                    continue
                }
                
                let attachmentStr = NSAttributedString(attachment: attachment)
                mutAttrString.replaceCharacters(in: match.range, with: attachmentStr)
            }
            
            return mutAttrString
        }
        
        if emojiRequests.isEmpty {
            // waiting on the group when there are only cached emojis results in a bad first layout,
            // because it would still wait until the next runloop iteration
            // but, since there were no requests, we can generate the correct string immediately
            let replacedString = buildStringWithEmojisReplaced(usePlaceholders: false)
            setAttributedString(replacedString, true)
        } else {
            // if we did find emojis and there are pending network requests,
            // set an attributed strings with placeholders for non-loaded emojis to avoid layout shift
            setAttributedString(buildStringWithEmojisReplaced(usePlaceholders: true), true)
            
            group.notify(queue: .main) { [weak self] in
                // if e.g. the account changes before all emojis are loaded, don't bother trying to set them
                guard let self = self, self.emojiIdentifier == identifier else { return }
                
                let replacedString = buildStringWithEmojisReplaced(usePlaceholders: false)
                setAttributedString(replacedString, true)
            }
        }
    }
    
    func replaceEmojis<ID: Hashable>(in string: String, emojis: [Emoji], identifier: ID?, setAttributedString: @escaping (_ attributedString: NSAttributedString, _ didReplaceEmojis: Bool) -> Void) {
        replaceEmojis(in: NSAttributedString(string: string), emojis: emojis, identifier: identifier, setAttributedString: setAttributedString)
    }
}
