//
//  TextUtils.swift
//  GraphCore
//
//  Created by Mikhail Filimonov on 26.02.2020.
//  Copyright © 2020 Telegram. All rights reserved.
//

import Foundation
#if os(macOS)
import Cocoa
#else
import UIKit
#endif

#if os(iOS)
typealias NSFont = UIFont
#endif

private let defaultFont:NSFont = NSFont.systemFont(ofSize: 14)

extension NSAttributedString {
    var size: CGSize {
        return textSize(with: self.string, font: self.attribute(.font, at: 0, effectiveRange: nil) as? NSFont ?? defaultFont)
    }
}

func textSize(with string: String, font: NSFont) -> CGSize {
    
    let attributedString:NSAttributedString = NSAttributedString(string: string, attributes: [.font : font])
    let layout = LabelNode.layoutText(attributedString, CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude))
    var size:CGSize = layout.0.size
    size.width = ceil(size.width)
    size.height = ceil(size.height)
    
    return size
}



private final class LabelNodeLine {
    let line: CTLine
    let frame: CGRect
    
    init(line: CTLine, frame: CGRect) {
        self.line = line
        self.frame = frame
    }
}


public final class LabelNodeLayout: NSObject {
    fileprivate let attributedString: NSAttributedString?
    fileprivate let truncationType: CTLineTruncationType
    fileprivate let constrainedSize: CGSize
    fileprivate let lines: [LabelNodeLine]
    
    let size: CGSize
    
    fileprivate init(attributedString: NSAttributedString?, truncationType: CTLineTruncationType, constrainedSize: CGSize, size: CGSize, lines: [LabelNodeLine]) {
        self.attributedString = attributedString
        self.truncationType = truncationType
        self.constrainedSize = constrainedSize
        self.size = size
        self.lines = lines
    }
    
    var numberOfLines: Int {
        return self.lines.count
    }
    
    var trailingLineWidth: CGFloat {
        if let lastLine = self.lines.last {
            return lastLine.frame.width
        } else {
            return 0.0
        }
    }
}

class LabelNode: NSObject {
    private var currentLayout: LabelNodeLayout?
    
    private class func getlayout(attributedString: NSAttributedString?, truncationType: CTLineTruncationType, constrainedSize: CGSize) -> LabelNodeLayout {
        
        if let attributedString = attributedString {
            let font: CTFont
            if attributedString.length != 0 {
                if let stringFont = attributedString.attribute(NSAttributedString.Key(kCTFontAttributeName as String), at: 0, effectiveRange: nil) {
                    font = stringFont as! CTFont
                } else if let f = attributedString.attribute(.font, at: 0, effectiveRange: nil) as? NSFont {
                    font = f
                } else {
                    font = defaultFont
                }
            } else {
                font = defaultFont
            }
            
            let fontAscent = CTFontGetAscent(font)
            let fontDescent = CTFontGetDescent(font)
            let fontLineHeight = floor(fontAscent + fontDescent)
            let fontLineSpacing = floor(fontLineHeight * 0.12)
            
            var lines: [LabelNodeLine] = []
            
            
            
            var maybeTypesetter: CTTypesetter?
            maybeTypesetter = CTTypesetterCreateWithAttributedString(attributedString as CFAttributedString)
            if maybeTypesetter == nil {
                return LabelNodeLayout(attributedString: attributedString, truncationType: truncationType, constrainedSize: constrainedSize, size: CGSize(), lines: [])
            }
            
            let typesetter = maybeTypesetter!
            var layoutSize = CGSize()

            let lineOriginY = floor(layoutSize.height + fontLineHeight - fontLineSpacing * 2.0)

            let lastLineCharacterIndex: CFIndex = 0
            
            let coreTextLine: CTLine
            
            let originalLine = CTTypesetterCreateLineWithOffset(typesetter, CFRange(location: lastLineCharacterIndex, length: attributedString.length - lastLineCharacterIndex), 0.0)
            
            if CTLineGetTypographicBounds(originalLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(originalLine) < Double(constrainedSize.width) {
                coreTextLine = originalLine
            } else {
                var truncationTokenAttributes: [NSAttributedString.Key : Any] = [:]
                truncationTokenAttributes[NSAttributedString.Key(kCTFontAttributeName as String)] = font
                truncationTokenAttributes[NSAttributedString.Key(kCTForegroundColorFromContextAttributeName as String)] = true as NSNumber
                let tokenString = "\u{2026}"
                let truncatedTokenString = NSAttributedString(string: tokenString, attributes: truncationTokenAttributes)
                let truncationToken = CTLineCreateWithAttributedString(truncatedTokenString)
                
                coreTextLine = CTLineCreateTruncatedLine(originalLine, Double(constrainedSize.width), truncationType, truncationToken) ?? truncationToken
                
            }
            
            let lineWidth = ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine)))
            let lineFrame = CGRect(x: 0, y: lineOriginY, width: lineWidth, height: fontLineHeight)
            layoutSize.height += fontLineHeight + fontLineSpacing
            layoutSize.width = max(layoutSize.width, lineWidth)
            
            lines.append(LabelNodeLine(line: coreTextLine, frame: lineFrame))
            
            return LabelNodeLayout(attributedString: attributedString, truncationType: truncationType, constrainedSize: constrainedSize, size: CGSize(width: ceil(layoutSize.width), height: ceil(layoutSize.height)), lines: lines)
        } else {
            return LabelNodeLayout(attributedString: attributedString, truncationType: truncationType, constrainedSize: constrainedSize, size: CGSize(), lines: [])
        }
    }
    
    
    func draw(_ dirtyRect: CGRect, in ctx: CGContext, backingScaleFactor: CGFloat) {
        
        ctx.saveGState()
        
        ctx.setAllowsFontSubpixelPositioning(true)
        ctx.setShouldSubpixelPositionFonts(true)
        
        ctx.setAllowsAntialiasing(true)
        ctx.setShouldAntialias(true)
        
        ctx.setAllowsFontSmoothing(backingScaleFactor == 1.0)
        ctx.setShouldSmoothFonts(backingScaleFactor == 1.0)
        
        let context:CGContext = ctx
        
        if let layout = self.currentLayout {
            let textMatrix = context.textMatrix
            let textPosition = context.textPosition
            context.textMatrix = CGAffineTransform(scaleX: 1.0, y: -1.0)
            for i in 0 ..< layout.lines.count {
                let line = layout.lines[i]
                context.textPosition = CGPoint(x: dirtyRect.minX, y: line.frame.origin.y + dirtyRect.minY)
                CTLineDraw(line.line, context)
            }
            
            context.textMatrix = textMatrix
            context.textPosition = CGPoint(x: textPosition.x, y: textPosition.y)
        }
        ctx.restoreGState()
    }
    
    
    
    class func layoutText(_ attributedString: NSAttributedString?, _ constrainedSize: CGSize, _ truncationType: CTLineTruncationType = .end) -> (LabelNodeLayout, LabelNode) {
        let layout: LabelNodeLayout
        layout = LabelNode.getlayout(attributedString: attributedString, truncationType: truncationType, constrainedSize: constrainedSize)
        let node = LabelNode()
        node.currentLayout = layout
        return (layout, node)
    }
}