//
//  HTMLConverter.swift
//  Tusker
//
//  Created by Shadowfacts on 12/3/22.
//  Copyright © 2022 Shadowfacts. All rights reserved.
//

import UIKit
import SwiftSoup
import WebURL
import WebURLFoundationExtras

struct HTMLConverter {
    
    static let defaultFont = UIFont.systemFont(ofSize: 17)
    static let defaultMonospaceFont = UIFont.monospacedSystemFont(ofSize: 17, weight: .regular)
    static let defaultColor = UIColor.label
    static let defaultParagraphStyle: NSParagraphStyle = {
        let style = NSMutableParagraphStyle()
        // 2 points is enough that it doesn't make things look weirdly spaced out, but leaves a slight gap when there are multiple lines of emojis
        style.lineSpacing = 2
        return style
    }()
    
    var font: UIFont = defaultFont
    var monospaceFont: UIFont = defaultMonospaceFont
    var color: UIColor = defaultColor
    var paragraphStyle: NSParagraphStyle = defaultParagraphStyle
    
    func convert(_ html: String) -> NSAttributedString {
        let doc = try! SwiftSoup.parseBodyFragment(html)
        let body = doc.body()!
        
        if let attributedText = attributedTextForHTMLNode(body) {
            let mutAttrString = NSMutableAttributedString(attributedString: attributedText)
            mutAttrString.trimTrailingCharactersInSet(.whitespacesAndNewlines)
            mutAttrString.collapseWhitespace()
            
            // Wait until the end and then fill in the unset paragraph styles, to avoid clobbering the list style.
            mutAttrString.enumerateAttribute(.paragraphStyle, in: mutAttrString.fullRange, options: .longestEffectiveRangeNotRequired) { value, range, stop in
                if value == nil {
                    mutAttrString.addAttribute(.paragraphStyle, value: paragraphStyle, range: range)
                }
            }
            
            return mutAttrString
        } else {
            return NSAttributedString()
        }
    }
    
    private func attributedTextForHTMLNode(_ node: Node, usePreformattedText: Bool = false) -> NSAttributedString? {
        switch node {
        case let node as TextNode:
            let text: String
            if usePreformattedText {
                text = node.getWholeText()
            } else {
                text = node.text()
            }
            return NSAttributedString(string: text, attributes: [.font: font, .foregroundColor: color])
        case let node as Element:
            if node.tagName() == "ol" || node.tagName() == "ul" {
                return attributedTextForList(node, usePreformattedText: usePreformattedText)
            }
            
            let attributed = NSMutableAttributedString(string: "", attributes: [.font: font, .foregroundColor: color])
            for child in node.getChildNodes() {
                var appendEllipsis = false
                if node.tagName() == "a",
                   let el = child as? Element {
                    if el.hasClass("invisible") {
                        continue
                    } else if el.hasClass("ellipsis") {
                        appendEllipsis = true
                    }
                }
                
                if let childText = attributedTextForHTMLNode(child, usePreformattedText: usePreformattedText || node.tagName() == "pre") {
                    attributed.append(childText)
                }
                
                if appendEllipsis {
                    attributed.append(NSAttributedString("…"))
                }
            }
            
            lazy var currentFont = if attributed.length == 0 {
                font
            } else {
                attributed.attribute(.font, at: 0, effectiveRange: nil) as? UIFont ?? font
            }
            
            switch node.tagName() {
            case "br":
                // need to specify defaultFont here b/c otherwise it uses the default 12pt Helvetica which
                // screws up its determination of the line height making multiple lines of emojis squash together
                attributed.append(NSAttributedString(string: "\n", attributes: [.font: font]))
            case "a":
                let href = try! node.attr("href")
                if let webURL = WebURL(href),
                   let url = URL(webURL) {
                    attributed.addAttribute(.link, value: url, range: attributed.fullRange)
                } else if let url = URL(string: href) {
                    attributed.addAttribute(.link, value: url, range: attributed.fullRange)
                }
            case "p":
                attributed.append(NSAttributedString(string: "\n\n", attributes: [.font: font]))
            case "em", "i":
                attributed.addAttribute(.font, value: currentFont.withTraits(.traitItalic)!, range: attributed.fullRange)
            case "strong", "b":
                attributed.addAttribute(.font, value: currentFont.withTraits(.traitBold)!, range: attributed.fullRange)
            case "del":
                attributed.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: attributed.fullRange)
            case "code":
                attributed.addAttribute(.font, value: monospaceFont, range: attributed.fullRange)
            case "pre":
                attributed.append(NSAttributedString(string: "\n\n"))
                attributed.addAttribute(.font, value: monospaceFont, range: attributed.fullRange)
            case "blockquote":
                let paragraphStyle = paragraphStyle.mutableCopy() as! NSMutableParagraphStyle
                paragraphStyle.headIndent = 32
                paragraphStyle.firstLineHeadIndent = 32
                attributed.addAttributes([
                    .font: currentFont.withTraits(.traitItalic)!,
                    .paragraphStyle: paragraphStyle,
                ], range: attributed.fullRange)
            default:
                break
            }
            
            return attributed
        default:
            return nil
        }
    }
    
    private func attributedTextForList(_ element: Element, usePreformattedText: Bool) -> NSAttributedString {
        let list = element.tagName() == "ol" ? OrderedNumberTextList(markerFormat: .decimal, options: 0) : NSTextList(markerFormat: .disc, options: 0)
        let paragraphStyle = paragraphStyle.mutableCopy() as! NSMutableParagraphStyle
        // I don't like that I can't just use paragraphStyle.textLists, because it makes the list markers
        // not use the monospace digit font (it seems to just use whatever font attribute is set for the whole thing),
        // and it doesn't right align the list markers.
        // Unfortunately, doing it manually means the list markers are incldued in the selectable text.
        paragraphStyle.headIndent = 32
        paragraphStyle.firstLineHeadIndent = 0
        // Use 2 tab stops, one for the list marker, the second for the content.
        paragraphStyle.tabStops = [NSTextTab(textAlignment: .right, location: 28), NSTextTab(textAlignment: .natural, location: 32)]
        let str = NSMutableAttributedString(string: "")
        var item = 1
        for child in element.children() where child.tagName() == "li" {
            if let childStr = attributedTextForHTMLNode(child, usePreformattedText: usePreformattedText) {
                str.append(NSAttributedString(string: "\t\(list.marker(forItemNumber: item))\t", attributes: [
                    .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .monospacedDigitSystemFont(ofSize: 17, weight: .regular)),
                ]))
                str.append(childStr)
                str.append(NSAttributedString(string: "\n"))
                item += 1
            }
        }
        str.addAttribute(.paragraphStyle, value: paragraphStyle, range: str.fullRange)
        return str
    }
    
}

private class OrderedNumberTextList: NSTextList {
    override func marker(forItemNumber itemNumber: Int) -> String {
        "\(super.marker(forItemNumber: itemNumber))."
    }
}
