import Foundation
import UIKit
import AsyncDisplayKit

private enum CornerType {
    case topLeft
    case topRight
    case bottomLeft
    case bottomRight
}

private func drawFullCorner(context: CGContext, color: UIColor, at point: CGPoint, type: CornerType, radius: CGFloat) {
    if radius.isZero {
        return
    }
    context.setFillColor(color.cgColor)
    switch type {
    case .topLeft:
        context.clear(CGRect(origin: point, size: CGSize(width: radius, height: radius)))
        context.fillEllipse(in: CGRect(origin: point, size: CGSize(width: radius * 2.0, height: radius * 2.0)))
    case .topRight:
        context.clear(CGRect(origin: CGPoint(x: point.x - radius, y: point.y), size: CGSize(width: radius, height: radius)))
        context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
    case .bottomLeft:
        context.clear(CGRect(origin: CGPoint(x: point.x, y: point.y - radius), size: CGSize(width: radius, height: radius)))
        context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
    case .bottomRight:
        context.clear(CGRect(origin: CGPoint(x: point.x - radius, y: point.y - radius), size: CGSize(width: radius, height: radius)))
        context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
    }
}

private func drawConnectingCorner(context: CGContext, color: UIColor, at point: CGPoint, type: CornerType, radius: CGFloat) {
    context.setFillColor(color.cgColor)
    switch type {
    case .topLeft:
        context.fill(CGRect(origin: CGPoint(x: point.x - radius, y: point.y), size: CGSize(width: radius, height: radius)))
        context.setFillColor(UIColor.clear.cgColor)
        context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
    case .topRight:
        context.fill(CGRect(origin: CGPoint(x: point.x, y: point.y), size: CGSize(width: radius, height: radius)))
        context.setFillColor(UIColor.clear.cgColor)
        context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
    case .bottomLeft:
        context.fill(CGRect(origin: CGPoint(x: point.x - radius, y: point.y - radius), size: CGSize(width: radius, height: radius)))
        context.setFillColor(UIColor.clear.cgColor)
        context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
    case .bottomRight:
        context.fill(CGRect(origin: CGPoint(x: point.x, y: point.y - radius), size: CGSize(width: radius, height: radius)))
        context.setFillColor(UIColor.clear.cgColor)
        context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
    }
}

public func generateRectsImage(color: UIColor, rects: [CGRect], inset: CGFloat, outerRadius: CGFloat, innerRadius: CGFloat, stroke: Bool = false, strokeWidth: CGFloat = 2.0, useModernPathCalculation: Bool) -> (CGPoint, UIImage?) {
    if rects.isEmpty {
        return (CGPoint(), nil)
    }
    
    var topLeft = rects[0].origin
    var bottomRight = CGPoint(x: rects[0].maxX, y: rects[0].maxY)
    for i in 1 ..< rects.count {
        topLeft.x = min(topLeft.x, rects[i].origin.x)
        topLeft.y = min(topLeft.y, rects[i].origin.y)
        bottomRight.x = max(bottomRight.x, rects[i].maxX)
        bottomRight.y = max(bottomRight.y, rects[i].maxY)
    }
    
    var drawingInset = inset
    if stroke {
        drawingInset += 2.0
    }
    
    topLeft.x -= drawingInset
    topLeft.y -= drawingInset
    bottomRight.x += drawingInset * 2.0
    bottomRight.y += drawingInset * 2.0
    
    return (topLeft, generateImage(CGSize(width: bottomRight.x - topLeft.x, height: bottomRight.y - topLeft.y), rotatedContext: { size, context in
        context.clear(CGRect(origin: CGPoint(), size: size))
        context.setFillColor(color.cgColor)
        
        context.setBlendMode(.copy)
        
        if useModernPathCalculation {
            if rects.count == 1 {
                let path = UIBezierPath(roundedRect: rects[0].offsetBy(dx: -topLeft.x, dy: -topLeft.y), cornerRadius: outerRadius).cgPath
                context.addPath(path)
                
                if stroke {
                    context.setStrokeColor(color.cgColor)
                    context.setLineWidth(strokeWidth)
                    context.strokePath()
                } else {
                    context.fillPath()
                }
                return
            }
            
            var combinedRects: [[CGRect]] = []
            var currentRects: [CGRect] = []
            for rect in rects {
                if rect.width.isZero {
                    if !currentRects.isEmpty {
                        combinedRects.append(currentRects)
                    }
                    currentRects.removeAll()
                } else {
                    currentRects.append(rect)
                }
            }
            if !currentRects.isEmpty {
                combinedRects.append(currentRects)
            }
            
            for rects in combinedRects {
                var rects = rects.map { $0.insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y) }
                
                let minRadius: CGFloat = 2.0
                
                for _ in 0 ..< rects.count * rects.count {
                    var hadChanges = false
                    for i in 0 ..< rects.count - 1 {
                        if rects[i].maxY > rects[i + 1].minY {
                            let midY = floor((rects[i].maxY + rects[i + 1].minY) * 0.5)
                            rects[i].size.height = midY - rects[i].minY
                            rects[i + 1].origin.y = midY
                            rects[i + 1].size.height = rects[i + 1].maxY - midY
                            hadChanges = true
                        }
                        if rects[i].maxY >= rects[i + 1].minY && rects[i].insetBy(dx: 0.0, dy: 1.0).intersects(rects[i + 1]) {
                            if abs(rects[i].minX - rects[i + 1].minX) < minRadius {
                                let commonMinX = min(rects[i].origin.x, rects[i + 1].origin.x)
                                if rects[i].origin.x != commonMinX {
                                    rects[i].origin.x = commonMinX
                                    hadChanges = true
                                }
                                if rects[i + 1].origin.x != commonMinX {
                                    rects[i + 1].origin.x = commonMinX
                                    hadChanges = true
                                }
                            }
                            if abs(rects[i].maxX - rects[i + 1].maxX) < minRadius {
                                let commonMaxX = max(rects[i].maxX, rects[i + 1].maxX)
                                if rects[i].maxX != commonMaxX {
                                    rects[i].size.width = commonMaxX - rects[i].minX
                                    hadChanges = true
                                }
                                if rects[i + 1].maxX != commonMaxX {
                                    rects[i + 1].size.width = commonMaxX - rects[i + 1].minX
                                    hadChanges = true
                                }
                            }
                        }
                    }
                    if !hadChanges {
                        break
                    }
                }
                
                context.move(to: CGPoint(x: rects[0].midX, y: rects[0].minY))
                context.addLine(to: CGPoint(x: rects[0].maxX - outerRadius, y: rects[0].minY))
                context.addArc(tangent1End: rects[0].topRight, tangent2End: CGPoint(x: rects[0].maxX, y: rects[0].minY + outerRadius), radius: outerRadius)
                context.addLine(to: CGPoint(x: rects[0].maxX, y: rects[0].midY))
                
                for i in 0 ..< rects.count - 1 {
                    let rect = rects[i]
                    let next = rects[i + 1]
                    
                    if rect.maxX == next.maxX {
                        context.addLine(to: CGPoint(x: next.maxX, y: next.midY))
                    } else {
                        let nextRadius = min(outerRadius, floor(abs(rect.maxX - next.maxX) * 0.5))
                        context.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - nextRadius))
                        if next.maxX > rect.maxX {
                            context.addArc(tangent1End: CGPoint(x: rect.maxX, y: rect.maxY), tangent2End: CGPoint(x: rect.maxX + nextRadius, y: rect.maxY), radius: nextRadius)
                            context.addLine(to: CGPoint(x: next.maxX - nextRadius, y: next.minY))
                        } else {
                            context.addArc(tangent1End: CGPoint(x: rect.maxX, y: rect.maxY), tangent2End: CGPoint(x: rect.maxX - nextRadius, y: rect.maxY), radius: nextRadius)
                            context.addLine(to: CGPoint(x: next.maxX + nextRadius, y: next.minY))
                        }
                        context.addArc(tangent1End: next.topRight, tangent2End: CGPoint(x: next.maxX, y: next.minY + nextRadius), radius: nextRadius)
                        context.addLine(to: CGPoint(x: next.maxX, y: next.midY))
                    }
                }
                
                let last = rects[rects.count - 1]
                context.addLine(to: CGPoint(x: last.maxX, y: last.maxY - outerRadius))
                context.addArc(tangent1End: last.bottomRight, tangent2End: CGPoint(x: last.maxX - outerRadius, y: last.maxY), radius: outerRadius)
                context.addLine(to: CGPoint(x: last.minX + outerRadius, y: last.maxY))
                context.addArc(tangent1End: last.bottomLeft, tangent2End: CGPoint(x: last.minX, y: last.maxY - outerRadius), radius: outerRadius)
                
                for i in (1 ..< rects.count).reversed() {
                    let rect = rects[i]
                    let prev = rects[i - 1]
                    
                    if rect.minX == prev.minX {
                        context.addLine(to: CGPoint(x: prev.minX, y: prev.midY))
                    } else {
                        let prevRadius = min(outerRadius, floor(abs(rect.minX - prev.minX) * 0.5))
                        context.addLine(to: CGPoint(x: rect.minX, y: rect.minY + prevRadius))
                        if rect.minX < prev.minX {
                            context.addArc(tangent1End: CGPoint(x: rect.minX, y: rect.minY), tangent2End: CGPoint(x: rect.minX + prevRadius, y: rect.minY), radius: prevRadius)
                            context.addLine(to: CGPoint(x: prev.minX - prevRadius, y: prev.maxY))
                        } else {
                            context.addArc(tangent1End: CGPoint(x: rect.minX, y: rect.minY), tangent2End: CGPoint(x: rect.minX - prevRadius, y: rect.minY), radius: prevRadius)
                            context.addLine(to: CGPoint(x: prev.minX + prevRadius, y: prev.maxY))
                        }
                        context.addArc(tangent1End: prev.bottomLeft, tangent2End: CGPoint(x: prev.minX, y: prev.maxY - prevRadius), radius: prevRadius)
                        context.addLine(to: CGPoint(x: prev.minX, y: prev.midY))
                    }
                }
                
                context.addLine(to: CGPoint(x: rects[0].minX, y: rects[0].minY + outerRadius))
                context.addArc(tangent1End: rects[0].topLeft, tangent2End: CGPoint(x: rects[0].minX + outerRadius, y: rects[0].minY), radius: outerRadius)
                context.addLine(to: CGPoint(x: rects[0].midX, y: rects[0].minY))
                
                if stroke {
                    context.setStrokeColor(color.cgColor)
                    context.setLineWidth(strokeWidth)
                    context.strokePath()
                } else {
                    context.fillPath()
                }
            }
            return
        }
        
        for i in 0 ..< rects.count {
            let rect = rects[i].insetBy(dx: -inset, dy: -inset)
            context.fill(rect.offsetBy(dx: -topLeft.x, dy: -topLeft.y))
        }
        
        for i in 0 ..< rects.count {
            let rect = rects[i].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y)
            
            var previous: CGRect?
            if i != 0 {
                previous = rects[i - 1].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y)
            }
            
            var next: CGRect?
            if i != rects.count - 1 {
                next = rects[i + 1].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y)
            }
            
            if let previous = previous {
                if previous.contains(rect.topLeft) {
                    if abs(rect.topLeft.x - previous.minX) >= innerRadius {
                        var radius = innerRadius
                        if let next = next {
                            radius = min(radius, floor((next.minY - previous.maxY) / 2.0))
                        }
                        drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.topLeft.x, y: previous.maxY), type: .topLeft, radius: radius)
                    }
                } else {
                    drawFullCorner(context: context, color: color, at: rect.topLeft, type: .topLeft, radius: outerRadius)
                }
                if previous.contains(rect.topRight.offsetBy(dx: -1.0, dy: 0.0)) {
                    if abs(rect.topRight.x - previous.maxX) >= innerRadius {
                        var radius = innerRadius
                        if let next = next {
                            radius = min(radius, floor((next.minY - previous.maxY) / 2.0))
                        }
                        drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.topRight.x, y: previous.maxY), type: .topRight, radius: radius)
                    }
                } else {
                    drawFullCorner(context: context, color: color, at: rect.topRight, type: .topRight, radius: outerRadius)
                }
            } else {
                drawFullCorner(context: context, color: color, at: rect.topLeft, type: .topLeft, radius: outerRadius)
                drawFullCorner(context: context, color: color, at: rect.topRight, type: .topRight, radius: outerRadius)
            }
            
            if let next = next {
                if next.contains(rect.bottomLeft) {
                    if abs(rect.bottomRight.x - next.maxX) >= innerRadius {
                        var radius = innerRadius
                        if let previous = previous {
                            radius = min(radius, floor((next.minY - previous.maxY) / 2.0))
                        }
                        drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.bottomLeft.x, y: next.minY), type: .bottomLeft, radius: radius)
                    }
                } else {
                    drawFullCorner(context: context, color: color, at: rect.bottomLeft, type: .bottomLeft, radius: outerRadius)
                }
                if next.contains(rect.bottomRight.offsetBy(dx: -1.0, dy: 0.0)) {
                    if abs(rect.bottomRight.x - next.maxX) >= innerRadius {
                        var radius = innerRadius
                        if let previous = previous {
                            radius = min(radius, floor((next.minY - previous.maxY) / 2.0))
                        }
                        drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.bottomRight.x, y: next.minY), type: .bottomRight, radius: radius)
                    }
                } else {
                    drawFullCorner(context: context, color: color, at: rect.bottomRight, type: .bottomRight, radius: outerRadius)
                }
            } else {
                drawFullCorner(context: context, color: color, at: rect.bottomLeft, type: .bottomLeft, radius: outerRadius)
                drawFullCorner(context: context, color: color, at: rect.bottomRight, type: .bottomRight, radius: outerRadius)
            }
        }
    }))
}

public final class LinkHighlightingNode: ASDisplayNode {
    public private(set) var rects: [CGRect] = []
    public let imageNode: ASImageNode
    
    public var innerRadius: CGFloat = 4.0
    public var outerRadius: CGFloat = 4.0
    public var inset: CGFloat = 2.0
    public var useModernPathCalculation: Bool = false
    public var borderOnly: Bool = false
    public var strokeWidth: CGFloat = 1.0
    
    private var _color: UIColor
    public var color: UIColor {
        get {
            return _color
        } set(value) {
            self._color = value
            if !self.rects.isEmpty {
                self.updateImage()
            }
        }
    }
    
    public init(color: UIColor) {
        self._color = color
        
        self.imageNode = ASImageNode()
        self.imageNode.isUserInteractionEnabled = false
        self.imageNode.displaysAsynchronously = false
        
        super.init()
        
        self.addSubnode(self.imageNode)
    }
    
    public func updateRects(_ rects: [CGRect], color: UIColor? = nil) {
        var updated = false
        if self.rects != rects {
            updated = true
            self.rects = rects
        }
        
        if let color, !color.isEqual(self.color) {
            updated = true
            self.color = color
        }
        
        if updated {
            self.updateImage()
        }
    }
    
    private func updateImage() {
        if self.rects.isEmpty {
            self.imageNode.image = nil
        }
        let (offset, image) = generateRectsImage(color: self.color, rects: self.rects, inset: self.inset, outerRadius: self.outerRadius, innerRadius: self.innerRadius, stroke: self.borderOnly, strokeWidth: self.strokeWidth, useModernPathCalculation: self.useModernPathCalculation)
        
        if let image = image {
            self.imageNode.image = image
            self.imageNode.frame = CGRect(origin: offset, size: image.size)
        }
    }

    public static func generateImage(color: UIColor, inset: CGFloat, innerRadius: CGFloat, outerRadius: CGFloat, rects: [CGRect], useModernPathCalculation: Bool) -> (CGPoint, UIImage)? {
        if rects.isEmpty {
           return nil
        }
        let (offset, image) = generateRectsImage(color: color, rects: rects, inset: inset, outerRadius: outerRadius, innerRadius: innerRadius, useModernPathCalculation: useModernPathCalculation)

        if let image = image {
            return (offset, image)
        } else {
            return nil
        }
    }
    
    public func asyncLayout() -> (UIColor, [CGRect], CGFloat, CGFloat, CGFloat) -> () -> Void {
        let currentRects = self.rects
        let currentColor = self._color
        let currentInnerRadius = self.innerRadius
        let currentOuterRadius = self.outerRadius
        let currentInset = self.inset
        let useModernPathCalculation = self.useModernPathCalculation
        
        return { [weak self] color, rects, innerRadius, outerRadius, inset in
            var updatedImage: (CGPoint, UIImage?)?
            if currentRects != rects || !currentColor.isEqual(color) || currentInnerRadius != innerRadius || currentOuterRadius != outerRadius || currentInset != inset {
                updatedImage = generateRectsImage(color: color, rects: rects, inset: inset, outerRadius: outerRadius, innerRadius: innerRadius, useModernPathCalculation: useModernPathCalculation)
            }
            
            return {
                if let strongSelf = self {
                    strongSelf._color = color
                    strongSelf.rects = rects
                    strongSelf.innerRadius = innerRadius
                    strongSelf.outerRadius = outerRadius
                    strongSelf.inset = inset
                    
                    if let (offset, maybeImage) = updatedImage, let image = maybeImage {
                        strongSelf.imageNode.image = image
                        strongSelf.imageNode.frame = CGRect(origin: offset, size: image.size)
                    }
                }
            }
        }
    }
}