import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import AnimationUI
import TelegramPresentationData

public final class MultilineText: Component {
    public let text: String
    public let font: UIFont
    public let color: UIColor

    public init(
        text: String,
        font: UIFont,
        color: UIColor
    ) {
        self.text = text
        self.font = font
        self.color = color
    }

    public static func ==(lhs: MultilineText, rhs: MultilineText) -> Bool {
        if lhs.text != rhs.text {
            return false
        }
        if lhs.font != rhs.font {
            return false
        }
        if lhs.color != rhs.color {
            return false
        }
        return true
    }

    public final class View: UIView {
        private let text: ImmediateTextNode

        init() {
            self.text = ImmediateTextNode()
            self.text.maximumNumberOfLines = 0

            super.init(frame: CGRect())

            self.addSubnode(self.text)
        }

        required init?(coder aDecoder: NSCoder) {
            preconditionFailure()
        }

        func update(component: MultilineText, availableSize: CGSize, environment: Environment<Empty>, transition: Transition) -> CGSize {
            self.text.attributedText = NSAttributedString(string: component.text, font: component.font, textColor: component.color, paragraphAlignment: nil)
            let textSize = self.text.updateLayout(availableSize)
            transition.setFrame(view: self.text.view, frame: CGRect(origin: CGPoint(), size: textSize))

            return textSize
        }
    }

    public func makeView() -> View {
        return View()
    }

    public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
        return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition)
    }
}

public final class LottieAnimationComponent: Component {
    public let name: String

    public init(
        name: String
    ) {
        self.name = name
    }

    public static func ==(lhs: LottieAnimationComponent, rhs: LottieAnimationComponent) -> Bool {
        if lhs.name != rhs.name {
            return false
        }
        return true
    }

    public final class View: UIView {
        private var animationNode: AnimationNode?
        private var currentName: String?

        init() {
            super.init(frame: CGRect())
        }

        required init?(coder aDecoder: NSCoder) {
            preconditionFailure()
        }

        func update(component: LottieAnimationComponent, availableSize: CGSize, environment: Environment<Empty>, transition: Transition) -> CGSize {
            if self.currentName != component.name {
                self.currentName = component.name

                if let animationNode = self.animationNode {
                    animationNode.removeFromSupernode()
                    self.animationNode = nil
                }

                let animationNode = AnimationNode(animation: component.name, colors: [:], scale: 1.0)
                self.animationNode = animationNode
                self.addSubnode(animationNode)

                animationNode.play()
            }

            if let animationNode = self.animationNode {
                let preferredSize = animationNode.preferredSize()
                return preferredSize ?? CGSize(width: 32.0, height: 32.0)
            } else {
                return CGSize()
            }
        }
    }

    public func makeView() -> View {
        return View()
    }

    public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
        return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition)
    }
}

private final class ScrollingTooltipAnimationComponent: Component {
    public init() {
    }

    public static func ==(lhs: ScrollingTooltipAnimationComponent, rhs: ScrollingTooltipAnimationComponent) -> Bool {
        return true
    }

    public final class View: UIView {
        private var progress: CGFloat = 0.0
        private var previousTarget: CGFloat = 0.0

        private var animator: DisplayLinkAnimator?

        init() {
            super.init(frame: CGRect())

            self.isOpaque = false
            self.backgroundColor = nil

            self.previousTarget = CGFloat.random(in: 0.0 ... 1.0)
            self.startNextAnimation()
        }

        required init?(coder aDecoder: NSCoder) {
            preconditionFailure()
        }

        func startNextAnimation() {
            self.animator?.invalidate()

            let previous = self.previousTarget
            let target = CGFloat.random(in: 0.0 ... 1.0)
            self.previousTarget = target
            let animator = DisplayLinkAnimator(duration: 1.0, from: previous, to: target, update: { [weak self] value in
                guard let strongSelf = self else {
                    return
                }

                strongSelf.progress = listViewAnimationCurveEaseInOut(value)
                strongSelf.setNeedsDisplay()
            }, completion: { [weak self] in
                Queue.mainQueue().after(0.3, {
                    guard let strongSelf = self else {
                        return
                    }

                    strongSelf.startNextAnimation()
                })
            })
            self.animator = animator
        }

        func update(component: ScrollingTooltipAnimationComponent, availableSize: CGSize, environment: Environment<Empty>, transition: Transition) -> CGSize {
            return CGSize(width: 32.0, height: 32.0)
        }

        override func draw(_ rect: CGRect) {
            guard let context = UIGraphicsGetCurrentContext() else {
                return
            }

            let progressValue = self.progress

            let itemSize: CGFloat = 12.0
            let itemSpacing: CGFloat = 1.0
            let listItemCount: CGFloat = 100.0
            let listHeight: CGFloat = itemSize * listItemCount + itemSpacing * (listItemCount - 1)

            context.setFillColor(UIColor(white: 1.0, alpha: 0.3).cgColor)

            let offset: CGFloat = progressValue * listHeight

            var minVisibleItemIndex: Int = Int(floor(offset / (itemSize + itemSpacing)))
            while true {
                let itemY: CGFloat = CGFloat(minVisibleItemIndex) * (itemSize + itemSpacing) - offset
                if itemY >= self.bounds.height {
                    break
                }
                for i in 0 ..< 2 {
                    UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: CGFloat(i) * (itemSize + itemSpacing), y: itemY), size: CGSize(width: itemSize, height: itemSize)), cornerRadius: 2.0).fill()
                }
                minVisibleItemIndex += 1
            }

            let gradientFraction: CGFloat = 10.0 / self.bounds.height

            let colorsArray: [CGColor] = ([
                UIColor(white: 1.0, alpha: 1.0),
                UIColor(white: 1.0, alpha: 0.0),
                UIColor(white: 1.0, alpha: 0.0),
                UIColor(white: 1.0, alpha: 1.0)
            ] as [UIColor]).map(\.cgColor)
            var locations: [CGFloat] = [0.0, gradientFraction, 1.0 - gradientFraction, 1.0]
            let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray as CFArray, locations: &locations)!
            context.setBlendMode(.destinationOut)

            context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: self.bounds.height), options: [])

            context.setBlendMode(.normal)
            context.setFillColor(UIColor.white.cgColor)

            let indicatorHeight: CGFloat = 10.0

            let indicatorMinY: CGFloat = 0.0
            let indicatorMaxY: CGFloat = self.bounds.height - indicatorHeight
            let indicatorX: CGFloat = (itemSize + itemSpacing) * 2.0
            let indicatorY = indicatorMinY * (1.0 - progress) + indicatorMaxY * progress
            UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: indicatorX, y: indicatorY), size: CGSize(width: 3.0, height: indicatorHeight)), cornerRadius: 1.5).fill()

            UIBezierPath(roundedRect: CGRect(x: indicatorX - 4.0 - 19.0, y: indicatorY + (indicatorHeight - 8.0) / 2.0, width: 19.0, height: 8.0), cornerRadius: 4.0).fill()
        }
    }

    public func makeView() -> View {
        return View()
    }

    public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
        return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition)
    }
}

public final class TooltipComponent: Component {
    public let icon: AnyComponent<Empty>?
    public let content: AnyComponent<Empty>
    public let arrowLocation: CGRect

    public init(
        icon: AnyComponent<Empty>?,
        content: AnyComponent<Empty>,
        arrowLocation: CGRect
    ) {
        self.icon = icon
        self.content = content
        self.arrowLocation = arrowLocation
    }

    public static func ==(lhs: TooltipComponent, rhs: TooltipComponent) -> Bool {
        if lhs.icon != rhs.icon {
            return false
        }
        if lhs.content != rhs.content {
            return false
        }
        if lhs.arrowLocation != rhs.arrowLocation {
            return false
        }
        return true
    }

    public final class View: UIView {
        private let backgroundView: UIView
        private let backgroundViewMask: UIImageView
        private var icon: ComponentHostView<Empty>?
        private let content: ComponentHostView<Empty>

        private let regularMaskImage: UIImage
        private let invertedMaskImage: UIImage

        init() {
            self.backgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
            self.backgroundViewMask = UIImageView()

            self.regularMaskImage = generateImage(CGSize(width: 42.0, height: 42.0), rotatedContext: { size, context in
                context.clear(CGRect(origin: CGPoint(), size: size))

                context.setFillColor(UIColor.black.cgColor)
                let _ = try? drawSvgPath(context, path: "M0,18.0252 C0,14.1279 0,12.1792 0.5358,10.609 C1.5362,7.6772 3.8388,5.3746 6.7706,4.3742 C8.3409,3.8384 10.2895,3.8384 14.1868,3.8384 L16.7927,3.8384 C18.2591,3.8384 18.9923,3.8384 19.7211,3.8207 C25.1911,3.6877 30.6172,2.8072 35.8485,1.2035 C36.5454,0.9899 37.241,0.758 38.6321,0.2943 C39.1202,0.1316 39.3643,0.0503 39.5299,0.0245 C40.8682,-0.184 42.0224,0.9702 41.8139,2.3085 C41.7881,2.4741 41.7067,2.7181 41.544,3.2062 C41.0803,4.5974 40.8485,5.293 40.6348,5.99 C39.0312,11.2213 38.1507,16.6473 38.0177,22.1173 C38,22.846 38,23.5793 38,25.0457 L38,27.6516 C38,31.5489 38,33.4975 37.4642,35.0677 C36.4638,37.9995 34.1612,40.3022 31.2294,41.3026 C29.6591,41.8384 27.7105,41.8384 23.8132,41.8384 L16,41.8384 C10.3995,41.8384 7.5992,41.8384 5.4601,40.7484 C3.5785,39.7897 2.0487,38.2599 1.0899,36.3783 C0,34.2392 0,31.4389 0,25.8384 L0,18.0252 Z ")
            })!.stretchableImage(withLeftCapWidth: 16, topCapHeight: 33)

            self.invertedMaskImage = generateImage(CGSize(width: 42.0, height: 42.0), contextGenerator: { size, context in
                context.clear(CGRect(origin: CGPoint(), size: size))

                context.setFillColor(UIColor.black.cgColor)
                let _ = try? drawSvgPath(context, path: "M0,18.0252 C0,14.1279 0,12.1792 0.5358,10.609 C1.5362,7.6772 3.8388,5.3746 6.7706,4.3742 C8.3409,3.8384 10.2895,3.8384 14.1868,3.8384 L16.7927,3.8384 C18.2591,3.8384 18.9923,3.8384 19.7211,3.8207 C25.1911,3.6877 30.6172,2.8072 35.8485,1.2035 C36.5454,0.9899 37.241,0.758 38.6321,0.2943 C39.1202,0.1316 39.3643,0.0503 39.5299,0.0245 C40.8682,-0.184 42.0224,0.9702 41.8139,2.3085 C41.7881,2.4741 41.7067,2.7181 41.544,3.2062 C41.0803,4.5974 40.8485,5.293 40.6348,5.99 C39.0312,11.2213 38.1507,16.6473 38.0177,22.1173 C38,22.846 38,23.5793 38,25.0457 L38,27.6516 C38,31.5489 38,33.4975 37.4642,35.0677 C36.4638,37.9995 34.1612,40.3022 31.2294,41.3026 C29.6591,41.8384 27.7105,41.8384 23.8132,41.8384 L16,41.8384 C10.3995,41.8384 7.5992,41.8384 5.4601,40.7484 C3.5785,39.7897 2.0487,38.2599 1.0899,36.3783 C0,34.2392 0,31.4389 0,25.8384 L0,18.0252 Z ")
            })!.stretchableImage(withLeftCapWidth: 16, topCapHeight: 9)

            self.backgroundViewMask.image = self.regularMaskImage

            self.content = ComponentHostView<Empty>()

            super.init(frame: CGRect())

            self.addSubview(self.backgroundView)
            self.backgroundView.mask = self.backgroundViewMask
            self.addSubview(self.content)
        }

        required init?(coder aDecoder: NSCoder) {
            preconditionFailure()
        }

        func update(component: TooltipComponent, availableSize: CGSize, environment: Environment<Empty>, transition: Transition) -> CGSize {
            let insets = UIEdgeInsets(top: 8.0, left: 8.0, bottom: 8.0, right: 8.0)
            let spacing: CGFloat = 8.0

            var iconSize: CGSize?
            if let icon = component.icon {
                let iconView: ComponentHostView<Empty>
                if let current = self.icon {
                    iconView = current
                } else {
                    iconView = ComponentHostView<Empty>()
                    self.icon = iconView
                    self.addSubview(iconView)
                }
                iconSize = iconView.update(
                    transition: transition,
                    component: icon,
                    environment: {},
                    containerSize: availableSize
                )
            } else if let icon = self.icon {
                self.icon = nil
                icon.removeFromSuperview()
            }

            var contentLeftInset: CGFloat = 0.0
            if let iconSize = iconSize {
                contentLeftInset += iconSize.width + spacing
            }

            let contentSize = self.content.update(
                transition: transition,
                component: component.content,
                environment: {},
                containerSize: CGSize(width: min(200.0, availableSize.width - contentLeftInset), height: availableSize.height)
            )

            var innerContentHeight = contentSize.height
            if let iconSize = iconSize, iconSize.height > innerContentHeight {
                innerContentHeight = iconSize.height
            }

            let combinedContentSize = CGSize(width: insets.left + insets.right + contentLeftInset + contentSize.width, height: insets.top + insets.bottom + innerContentHeight)
            var contentRect = CGRect(origin: CGPoint(x: component.arrowLocation.minX - combinedContentSize.width, y: component.arrowLocation.maxY), size: combinedContentSize)
            if contentRect.minX < 0.0 {
                contentRect.origin.x = component.arrowLocation.maxX
            }

            let maskedBackgroundFrame: CGRect

            if contentRect.maxY > availableSize.height {
                self.backgroundViewMask.image = self.invertedMaskImage
                contentRect.origin.y = component.arrowLocation.minY - contentRect.height - 4.0
                maskedBackgroundFrame = CGRect(origin: CGPoint(x: contentRect.minX, y: contentRect.minY - 4.0 + 3.0), size: CGSize(width: contentRect.width + 4.0, height: contentRect.height + 8.0))
                self.backgroundViewMask.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: maskedBackgroundFrame.size)
            } else {
                self.backgroundViewMask.image = self.regularMaskImage
                maskedBackgroundFrame = CGRect(origin: CGPoint(x: contentRect.minX, y: contentRect.minY - 4.0), size: CGSize(width: contentRect.width + 4.0, height: contentRect.height + 4.0))
                self.backgroundViewMask.frame = CGRect(origin: CGPoint(), size: maskedBackgroundFrame.size)
            }

            self.backgroundView.frame = maskedBackgroundFrame

            if let iconSize = iconSize, let icon = self.icon {
                transition.setFrame(view: icon, frame: CGRect(origin: CGPoint(x: contentRect.minX + insets.left, y: contentRect.minY + insets.top + floor((contentRect.height - insets.top - insets.bottom - iconSize.height) / 2.0)), size: iconSize))
            }
            transition.setFrame(view: self.content, frame: CGRect(origin: CGPoint(x: contentRect.minX + insets.left + contentLeftInset, y: contentRect.minY + insets.top + floor((contentRect.height - insets.top - insets.bottom - contentSize.height) / 2.0)), size: contentSize))

            return availableSize
        }
    }

    public func makeView() -> View {
        return View()
    }

    public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
        return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition)
    }
}

private final class RoundedRectangle: Component {
    let color: UIColor

    init(color: UIColor) {
        self.color = color
    }

    static func ==(lhs: RoundedRectangle, rhs: RoundedRectangle) -> Bool {
        if !lhs.color.isEqual(rhs.color) {
            return false
        }
        return true
    }

    final class View: UIView {
        private let backgroundView: UIImageView

        private var currentColor: UIColor?
        private var currentDiameter: CGFloat?

        init() {
            self.backgroundView = UIImageView()

            super.init(frame: CGRect())

            self.addSubview(self.backgroundView)
        }

        required init?(coder aDecoder: NSCoder) {
            preconditionFailure()
        }

        func update(component: RoundedRectangle, availableSize: CGSize, environment: Environment<Empty>, transition: Transition) -> CGSize {
            let shadowInset: CGFloat = 0.0
            let diameter = min(availableSize.width, availableSize.height)

            var updated = false
            if let currentColor = self.currentColor {
                if !component.color.isEqual(currentColor) {
                    updated = true
                }
            } else {
                updated = true
            }

            let diameterUpdated = self.currentDiameter != diameter
            if self.currentDiameter != diameter || updated {
                self.currentDiameter = diameter
                self.currentColor = component.color

                self.backgroundView.image = generateImage(CGSize(width: diameter + shadowInset * 2.0, height: diameter + shadowInset * 2.0), rotatedContext: { size, context in
                    context.clear(CGRect(origin: CGPoint(), size: size))

                    context.setFillColor(component.color.cgColor)

                    context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowInset, y: shadowInset), size: CGSize(width: size.width - shadowInset * 2.0, height: size.height - shadowInset * 2.0)))
                })?.stretchableImage(withLeftCapWidth: Int(diameter + shadowInset * 2.0) / 2, topCapHeight: Int(diameter + shadowInset * 2.0) / 2)
            }

            transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: -shadowInset, y: -shadowInset), size: CGSize(width: availableSize.width + shadowInset * 2.0, height: availableSize.height + shadowInset * 2.0)))

            let _ = diameterUpdated

            return availableSize
        }
    }

    func makeView() -> View {
        return View()
    }

    func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
        return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition)
    }
}

private let shadowInset: CGFloat = 10.0
private final class ShadowRoundedRectangle: Component {
    let color: UIColor

    init(color: UIColor) {
        self.color = color
    }

    static func ==(lhs: ShadowRoundedRectangle, rhs: ShadowRoundedRectangle) -> Bool {
        if !lhs.color.isEqual(rhs.color) {
            return false
        }
        return true
    }

    final class View: UIView {
        private let backgroundView: UIImageView

        private var currentColor: UIColor?
        private var currentDiameter: CGFloat?

        init() {
            self.backgroundView = UIImageView()

            super.init(frame: CGRect())

            self.addSubview(self.backgroundView)
        }

        required init?(coder aDecoder: NSCoder) {
            preconditionFailure()
        }

        func update(component: ShadowRoundedRectangle, availableSize: CGSize, environment: Environment<Empty>, transition: Transition) -> CGSize {
            let diameter = min(availableSize.width, availableSize.height)

            var updated = false
            if let currentColor = self.currentColor {
                if !component.color.isEqual(currentColor) {
                    updated = true
                }
            } else {
                updated = true
            }

            if self.currentDiameter != diameter || updated {
                self.currentDiameter = diameter
                self.currentColor = component.color

                self.backgroundView.image = generateImage(CGSize(width: diameter + shadowInset * 2.0, height: diameter + shadowInset * 2.0), rotatedContext: { size, context in
                    context.clear(CGRect(origin: CGPoint(), size: size))

                    context.setFillColor(component.color.cgColor)
                    context.setShadow(offset: CGSize(width: 0.0, height: -1.0), blur: 4.0, color: UIColor(white: 0.0, alpha: 0.2).cgColor)

                    context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowInset, y: shadowInset), size: CGSize(width: size.width - shadowInset * 2.0, height: size.height - shadowInset * 2.0)))
                })?.stretchableImage(withLeftCapWidth: Int(diameter + shadowInset * 2.0) / 2, topCapHeight: Int(diameter + shadowInset * 2.0) / 2)
            }

            let shadowFrame = CGRect(origin: CGPoint(x: -shadowInset, y: -shadowInset), size: CGSize(width: availableSize.width + shadowInset * 2.0, height: availableSize.height + shadowInset * 2.0))
            transition.setFrame(view: self.backgroundView, frame: shadowFrame)

            return shadowFrame.size
        }
    }

    func makeView() -> View {
        return View()
    }

    func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
        return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition)
    }
}

public final class RollingText: Component {
    public enum AnimationDirection {
        case up
        case down
    }
    
    private final class MeasureState: Equatable {
        let attributedText: NSAttributedString
        let availableSize: CGSize
        let size: CGSize
        
        init(attributedText: NSAttributedString, availableSize: CGSize, size: CGSize) {
            self.attributedText = attributedText
            self.availableSize = availableSize
            self.size = size
        }

        static func ==(lhs: MeasureState, rhs: MeasureState) -> Bool {
            if !lhs.attributedText.isEqual(rhs.attributedText) {
                return false
            }
            if lhs.availableSize != rhs.availableSize {
                return false
            }
            if lhs.size != rhs.size {
                return false
            }
            return true
        }
    }
    
    public final class View: UIView {
        private var measureState: MeasureState?
        private var containerView: UIImageView
        
        private var snapshotView: UIView?

        public override init(frame: CGRect) {
            self.containerView = UIImageView()
            
            super.init(frame: frame)
            
            self.addSubview(self.containerView)
        }
        
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
        
        public func update(component: RollingText, availableSize: CGSize) -> CGSize {
            let attributedText = NSAttributedString(string: component.text, attributes: [
                NSAttributedString.Key.font: component.font,
                NSAttributedString.Key.foregroundColor: component.color
            ])
            
            if let measureState = self.measureState {
                if measureState.attributedText.isEqual(to: attributedText) && measureState.availableSize == availableSize {
                    return measureState.size
                }
            }
            
            var boundingRect = attributedText.boundingRect(with: availableSize, options: .usesLineFragmentOrigin, context: nil)
            boundingRect.size.width = ceil(boundingRect.size.width)
            boundingRect.size.height = ceil(boundingRect.size.height)
            
            if let animation = component.animation {
                if let snapshotView = self.snapshotView {
                    self.snapshotView = nil
                    snapshotView.removeFromSuperview()
                    
                    self.containerView.layer.removeAnimation(forKey: "opacity")
                }
                if let snapshotView = self.containerView.snapshotContentTree() {
                    let horizontalOffset = boundingRect.width - snapshotView.frame.width
                    let verticalOffset: CGFloat = 12.0
                    
                    snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
                    snapshotView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: horizontalOffset, y: animation == .up ? verticalOffset : -verticalOffset), duration: 0.2, removeOnCompletion: false, additive: true, completion: { [weak self, weak snapshotView] _ in
                        self?.snapshotView = nil
                        snapshotView?.removeFromSuperview()
                    })
                        
                    self.addSubview(snapshotView)
                    self.snapshotView = snapshotView
                    
                    self.containerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
                    self.containerView.layer.animatePosition(from: CGPoint(x: -horizontalOffset, y: animation == .up ? -verticalOffset : verticalOffset), to: CGPoint(), duration: 0.2, additive: true)
                }
            }

            self.containerView.frame = CGRect(origin: CGPoint(), size: boundingRect.size)
            
            let measureState = MeasureState(attributedText: attributedText, availableSize: availableSize, size: boundingRect.size)
            if #available(iOS 10.0, *) {
                let renderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: measureState.size))
                let image = renderer.image { context in
                    UIGraphicsPushContext(context.cgContext)
                    measureState.attributedText.draw(at: CGPoint())
                    UIGraphicsPopContext()
                }
                self.containerView.image = image
            } else {
                UIGraphicsBeginImageContextWithOptions(measureState.size, false, 0.0)
                measureState.attributedText.draw(at: CGPoint())
                self.containerView.image = UIGraphicsGetImageFromCurrentImageContext()
                UIGraphicsEndImageContext()
            }

            self.measureState = measureState
            
            return boundingRect.size
        }
    }
    
    public let text: String
    public let font: UIFont
    public let color: UIColor
    public let animation: AnimationDirection?
    
    public init(text: String, font: UIFont, color: UIColor, animation: AnimationDirection?) {
        self.text = text
        self.font = font
        self.color = color
        self.animation = animation
    }

    public static func ==(lhs: RollingText, rhs: RollingText) -> Bool {
        if lhs.text != rhs.text {
            return false
        }
        if !lhs.font.isEqual(rhs.font) {
            return false
        }
        if !lhs.color.isEqual(rhs.color) {
            return false
        }
        if lhs.animation != rhs.animation {
            return false
        }
        return true
    }
    
    public func makeView() -> View {
        return View()
    }
    
    public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
        return view.update(component: self, availableSize: availableSize)
    }
}


final class SparseItemGridScrollingIndicatorComponent: CombinedComponent {
    let backgroundColor: UIColor
    let shadowColor: UIColor
    let foregroundColor: UIColor
    let date: (String, Int32)
    let previousDate: (String, Int32)?

    init(
        backgroundColor: UIColor,
        shadowColor: UIColor,
        foregroundColor: UIColor,
        date: (String, Int32),
        previousDate: (String, Int32)?
    ) {
        self.backgroundColor = backgroundColor
        self.shadowColor = shadowColor
        self.foregroundColor = foregroundColor
        self.date = date
        self.previousDate = previousDate
    }

    static func ==(lhs: SparseItemGridScrollingIndicatorComponent, rhs: SparseItemGridScrollingIndicatorComponent) -> Bool {
        if lhs.backgroundColor != rhs.backgroundColor {
            return false
        }
        if lhs.shadowColor != rhs.shadowColor {
            return false
        }
        if lhs.foregroundColor != rhs.foregroundColor {
            return false
        }
        if lhs.date != rhs.date {
            return false
        }
        if lhs.previousDate?.0 != rhs.previousDate?.0 || lhs.previousDate?.1 != rhs.previousDate?.1 {
            return false
        }
        return true
    }

    static var body: Body {
        let rect = Child(ShadowRoundedRectangle.self)
        let textMonth = Child(RollingText.self)
        let textYear = Child(RollingText.self)

        return { context in
            context.view.clipsToBounds = true
            
            let date = context.component.date
            
            let components = date.0.components(separatedBy: " ")
            let month = String(components.prefix(upTo: components.count - 1).joined(separator: " "))
            let year = components.last ?? ""
            
            var monthAnimation: RollingText.AnimationDirection?
            var yearAnimation: RollingText.AnimationDirection?
            
            if let previousDate = context.component.previousDate {
                func unpackDateTag(_ packedValue: Int32) -> (month: Int32, year: Int32) {
                    let year = Int32(bitPattern: (UInt32(bitPattern: packedValue) >> 0) & 0xffff)
                    let month = Int32(bitPattern: (UInt32(bitPattern: packedValue) >> 16) & 0xffff)
                    return (month, year)
                }
                let currentValue = unpackDateTag(date.1)
                let previousValue = unpackDateTag(previousDate.1)
                
                if currentValue.year != previousValue.year {
                    yearAnimation = currentValue.year > previousValue.year ? .up : .down
                    monthAnimation = yearAnimation
                } else if currentValue.month != previousValue.month {
                    monthAnimation = currentValue.month > previousValue.month ? .up : .down
                }
            }
            
            let textMonth = textMonth.update(
                component: RollingText(
                    text: month,
                    font: Font.with(size: 13.0, design: .regular, weight: .medium, traits: .monospacedNumbers),
                    color: context.component.foregroundColor,
                    animation: monthAnimation
                ),
                availableSize: CGSize(width: 200.0, height: 100.0),
                transition: .immediate
            )
            
            let textYear = textYear.update(
                component: RollingText(
                    text: year,
                    font: Font.with(size: 13.0, design: .regular, weight: .medium, traits: .monospacedNumbers),
                    color: context.component.foregroundColor,
                    animation: yearAnimation
                ),
                availableSize: CGSize(width: 200.0, height: 100.0),
                transition: .immediate
            )

            let rect = rect.update(
                component: ShadowRoundedRectangle(
                    color: context.component.backgroundColor
                ),
                availableSize: CGSize(width: textMonth.size.width + 3.0 + textYear.size.width + 26.0, height: 32.0),
                transition: .easeInOut(duration: 0.2)
            )

            let rectFrame = rect.size.centered(around: CGPoint(
                x: rect.size.width / 2.0,
                y: rect.size.height / 2.0
            ))

            context.add(rect
                .position(CGPoint(x: rectFrame.midX + shadowInset, y: rectFrame.midY + shadowInset))
            )
            
            let offset = CGSize(width: textMonth.size.width + 3.0 + textYear.size.width, height: textMonth.size.height).centered(in: rectFrame)

            let monthTextFrame = textMonth.size.leftCentered(in: rectFrame).offsetBy(dx: offset.minX, dy: 0.0)
            let yearTextFrame = textYear.size.leftCentered(in: rectFrame).offsetBy(dx: offset.minX + monthTextFrame.width + 3.0, dy: 0.0)
            context.add(textMonth
                .position(CGPoint(x: monthTextFrame.midX, y: monthTextFrame.midY))
            )
            context.add(textYear
                .position(CGPoint(x: yearTextFrame.midX, y: yearTextFrame.midY))
            )
            
            return rect.size
        }
    }
}

public final class SparseItemGridScrollingArea: ASDisplayNode {
    private final class DragGesture: UIGestureRecognizer {
        private let shouldBegin: (CGPoint) -> Bool
        private let began: () -> Void
        private let ended: () -> Void
        private let moved: (CGFloat) -> Void

        private var initialLocation: CGPoint?

        public init(
            shouldBegin: @escaping (CGPoint) -> Bool,
            began: @escaping () -> Void,
            ended: @escaping () -> Void,
            moved: @escaping (CGFloat) -> Void
        ) {
            self.shouldBegin = shouldBegin
            self.began = began
            self.ended = ended
            self.moved = moved

            super.init(target: nil, action: nil)
        }

        deinit {
        }

        override public func reset() {
            super.reset()

            self.initialLocation = nil
            self.initialLocation = nil
        }

        override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
            super.touchesBegan(touches, with: event)

            if self.numberOfTouches > 1 {
                self.state = .failed
                self.ended()
                return
            }

            if self.state == .possible {
                if let location = touches.first?.location(in: self.view) {
                    if self.shouldBegin(location) {
                        self.initialLocation = location
                        self.state = .began
                        self.began()
                    } else {
                        self.state = .failed
                    }
                } else {
                    self.state = .failed
                }
            }
        }

        override public func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
            super.touchesEnded(touches, with: event)

            self.initialLocation = nil

            if self.state == .began || self.state == .changed {
                self.ended()
                self.state = .failed
            }
        }

        override public func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
            super.touchesCancelled(touches, with: event)

            self.initialLocation = nil

            if self.state == .began || self.state == .changed {
                self.ended()
                self.state = .failed
            }
        }

        override public func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
            super.touchesMoved(touches, with: event)

            if (self.state == .began || self.state == .changed), let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) {
                self.state = .changed
                let offset = location.y - initialLocation.y
                self.moved(offset)
            }
        }
    }

    private let dateIndicatorContainer: UIView
    private let dateIndicator: ComponentHostView<Empty>

    private let lineIndicator: ComponentHostView<Empty>

    private var displayedTooltip: Bool = false
    private var lineTooltip: ComponentHostView<Empty>?

    private var containerSize: CGSize?
    private var indicatorPosition: CGFloat?
    private var scrollIndicatorHeight: CGFloat?

    private var dragGesture: DragGesture?
    public private(set) var isDragging: Bool = false

    private weak var draggingScrollView: UIScrollView?
    private var scrollingInitialOffset: CGFloat?

    private var activityTimer: SwiftSignalKit.Timer?

    public var beginScrolling: (() -> UIScrollView?)?
    public var finishedScrolling: (() -> Void)?
    public var setContentOffset: ((CGPoint) -> Void)?
    public var openCurrentDate: (() -> Void)?

    private var offsetBarTimer: SwiftSignalKit.Timer?
    private var beganAtDateIndicator = false
    private let hapticFeedback = HapticFeedback()

    private struct ProjectionData {
        var minY: CGFloat
        var maxY: CGFloat
        var indicatorHeight: CGFloat
        var scrollableHeight: CGFloat
    }
    private var projectionData: ProjectionData?

    public struct DisplayTooltip {
        public var animation: String?
        public var text: String
        public var completed: () -> Void

        public init(animation: String?, text: String, completed: @escaping () -> Void) {
            self.animation = animation
            self.text = text
            self.completed = completed
        }
    }

    public var displayTooltip: DisplayTooltip?

    private var theme: PresentationTheme?

    override public init() {
        self.dateIndicatorContainer = UIView()
        self.dateIndicatorContainer.isUserInteractionEnabled = false
        
        self.dateIndicator = ComponentHostView<Empty>()
        self.lineIndicator = ComponentHostView<Empty>()

        self.dateIndicator.alpha = 0.0
        self.lineIndicator.alpha = 0.0

        super.init()

        self.dateIndicator.isUserInteractionEnabled = false
        self.lineIndicator.isUserInteractionEnabled = false

        self.view.addSubview(self.dateIndicatorContainer)
        self.dateIndicatorContainer.addSubview(self.dateIndicator)
        self.view.addSubview(self.lineIndicator)

        let dragGesture = DragGesture(
            shouldBegin: { [weak self] point in
                guard let strongSelf = self else {
                    return false
                }

                if strongSelf.dateIndicator.frame.contains(point) {
                    strongSelf.beganAtDateIndicator = true
                } else {
                    strongSelf.beganAtDateIndicator = false
                }

                return true
            },
            began: { [weak self] in
                guard let strongSelf = self else {
                    return
                }

                let offsetBarTimer = SwiftSignalKit.Timer(timeout: 0.2, repeat: false, completion: {
                    guard let strongSelf = self else {
                        return
                    }
                    strongSelf.performOffsetBarTimerEvent()
                }, queue: .mainQueue())
                strongSelf.offsetBarTimer?.invalidate()
                strongSelf.offsetBarTimer = offsetBarTimer
                offsetBarTimer.start()

                strongSelf.isDragging = true

                if let scrollView = strongSelf.beginScrolling?() {
                    strongSelf.draggingScrollView = scrollView
                    strongSelf.scrollingInitialOffset = scrollView.contentOffset.y
                    strongSelf.setContentOffset?(scrollView.contentOffset)
                }

                strongSelf.updateActivityTimer(isScrolling: false)
                strongSelf.dismissLineTooltip()
            },
            ended: { [weak self] in
                guard let strongSelf = self else {
                    return
                }
                strongSelf.draggingScrollView = nil

                if strongSelf.offsetBarTimer != nil {
                    strongSelf.offsetBarTimer?.invalidate()
                    strongSelf.offsetBarTimer = nil

                    strongSelf.openCurrentDate?()
                }

                let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut)
                transition.updateSublayerTransformOffset(layer: strongSelf.dateIndicator.layer, offset: CGPoint(x: 0.0, y: 0.0))

                strongSelf.isDragging = false

                strongSelf.updateLineIndicator(transition: transition)

                strongSelf.updateActivityTimer(isScrolling: false)
                
                strongSelf.finishedScrolling?()
            },
            moved: { [weak self] relativeOffset in
                guard let strongSelf = self else {
                    return
                }
                guard let scrollView = strongSelf.draggingScrollView, let scrollingInitialOffset = strongSelf.scrollingInitialOffset else {
                    return
                }
                guard let projectionData = strongSelf.projectionData else {
                    return
                }

                if strongSelf.offsetBarTimer != nil {
                    strongSelf.offsetBarTimer?.invalidate()
                    strongSelf.offsetBarTimer = nil
                    strongSelf.performOffsetBarTimerEvent()
                }

                let indicatorArea = projectionData.maxY - projectionData.minY
                let scrollFraction = projectionData.scrollableHeight / indicatorArea

                var offset = scrollingInitialOffset + scrollFraction * relativeOffset
                if offset < 0.0 {
                    offset = 0.0
                }
                if offset > scrollView.contentSize.height - scrollView.bounds.height {
                    offset = scrollView.contentSize.height - scrollView.bounds.height
                }

                strongSelf.setContentOffset?(CGPoint(x: 0.0, y: offset))
                let _ = scrollView
                let _ = projectionData
            }
        )
        self.dragGesture = dragGesture

        self.view.addGestureRecognizer(dragGesture)
    }

    private func performOffsetBarTimerEvent() {
        self.hapticFeedback.impact()
        self.offsetBarTimer = nil

        let transition: ContainedViewLayoutTransition = .animated(duration: 0.1, curve: .easeInOut)
        transition.updateSublayerTransformOffset(layer: self.dateIndicator.layer, offset: CGPoint(x: -80.0, y: 0.0))
        self.updateLineIndicator(transition: transition)
    }

    public func feedbackTap() {
        self.hapticFeedback.tap()
    }

    private var currentDate: (String, Int32)?
    
    public func update(
        containerSize: CGSize,
        containerInsets: UIEdgeInsets,
        contentHeight: CGFloat,
        contentOffset: CGFloat,
        isScrolling: Bool,
        date: (String, Int32),
        theme: PresentationTheme,
        transition: ContainedViewLayoutTransition
    ) {
        self.containerSize = containerSize
        self.theme = theme
        let previousDate = self.currentDate
        self.currentDate = date

        if self.dateIndicator.alpha.isZero {
            let transition: ContainedViewLayoutTransition = .immediate
            transition.updateSublayerTransformOffset(layer: self.dateIndicator.layer, offset: CGPoint())
        }

        if isScrolling {
            self.updateActivityTimer(isScrolling: true)
        }

        let animateIndicatorFrame = previousDate != nil && previousDate?.1 != date.1
        let indicatorSize = self.dateIndicator.update(
            transition: animateIndicatorFrame ? .easeInOut(duration: 0.2) : .immediate,
            component: AnyComponent(SparseItemGridScrollingIndicatorComponent(
                backgroundColor: theme.list.itemBlocksBackgroundColor,
                shadowColor: .black,
                foregroundColor: theme.list.itemPrimaryTextColor,
                date: date,
                previousDate: previousDate
            )),
            environment: {},
            containerSize: containerSize
        )

        let scrollIndicatorHeightFraction = min(1.0, max(0.0, (containerSize.height - containerInsets.top - containerInsets.bottom) / contentHeight))
        if scrollIndicatorHeightFraction >= 0.55 - .ulpOfOne {
            self.dateIndicator.isHidden = true
            self.lineIndicator.isHidden = true
        } else {
            self.dateIndicator.isHidden = false
            self.lineIndicator.isHidden = false
        }

        let indicatorVerticalInset: CGFloat = 3.0
        let topIndicatorInset: CGFloat = indicatorVerticalInset + containerInsets.top
        let bottomIndicatorInset: CGFloat = indicatorVerticalInset + containerInsets.bottom

        let scrollIndicatorHeight: CGFloat = 44.0

        let indicatorPositionFraction = min(1.0, max(0.0, contentOffset / (contentHeight - containerSize.height)))

        let indicatorTopPosition = topIndicatorInset
        let indicatorBottomPosition = containerSize.height - bottomIndicatorInset - scrollIndicatorHeight

        let dateIndicatorTopPosition = topIndicatorInset + floor(scrollIndicatorHeight - indicatorSize.height) / 2.0
        let dateIndicatorBottomPosition = containerSize.height - bottomIndicatorInset - floor(scrollIndicatorHeight - indicatorSize.height) / 2.0 - indicatorSize.height

        self.indicatorPosition = indicatorTopPosition * (1.0 - indicatorPositionFraction) + indicatorBottomPosition * indicatorPositionFraction
        self.scrollIndicatorHeight = scrollIndicatorHeight

        let dateIndicatorPosition = dateIndicatorTopPosition * (1.0 - indicatorPositionFraction) + dateIndicatorBottomPosition * indicatorPositionFraction - UIScreenPixel

        self.projectionData = ProjectionData(
            minY: dateIndicatorTopPosition,
            maxY: dateIndicatorBottomPosition,
            indicatorHeight: indicatorSize.height,
            scrollableHeight: contentHeight - containerSize.height
        )

        self.dateIndicatorContainer.frame = CGRect(origin: CGPoint(x: 0.0, y: dateIndicatorPosition), size: CGSize(width: containerSize.width, height: indicatorSize.height))
        
        var indicatorFrameTransition = transition
        if animateIndicatorFrame {
            indicatorFrameTransition = .animated(duration: 0.2, curve: .easeInOut)
        }
        indicatorFrameTransition.updateFrame(view: self.dateIndicator, frame: CGRect(origin: CGPoint(x: containerSize.width - 12.0 - indicatorSize.width, y: 0.0), size: indicatorSize))
    
        if isScrolling {
            let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut)
            transition.updateAlpha(layer: self.dateIndicator.layer, alpha: 1.0)
            transition.updateAlpha(layer: self.lineIndicator.layer, alpha: 1.0)
        }

        self.updateLineTooltip(containerSize: containerSize)

        self.updateLineIndicator(transition: transition)

        if isScrolling && !self.dateIndicator.isHidden {
            self.displayTooltipOnFirstScroll()
        }
    }

    private func updateLineIndicator(transition: ContainedViewLayoutTransition) {
        guard let indicatorPosition = self.indicatorPosition, let scrollIndicatorHeight = self.scrollIndicatorHeight, let theme = self.theme else {
            return
        }

        let lineIndicatorSize = CGSize(width: (self.isDragging || self.lineTooltip != nil) ? 6.0 : 3.0, height: scrollIndicatorHeight)
        let mappedTransition: Transition
        switch transition {
        case .immediate:
            mappedTransition = .immediate
        case let .animated(duration, _):
            mappedTransition = Transition(animation: .curve(duration: duration, curve: .easeInOut))
        }
        let _ = self.lineIndicator.update(
            transition: mappedTransition,
            component: AnyComponent(RoundedRectangle(
                color: theme.list.scrollIndicatorColor
            )),
            environment: {},
            containerSize: lineIndicatorSize
        )

        transition.updateFrame(view: self.lineIndicator, frame: CGRect(origin: CGPoint(x: self.bounds.size.width - 3.0 - lineIndicatorSize.width, y: indicatorPosition), size: lineIndicatorSize))
    }

    private func updateActivityTimer(isScrolling: Bool) {
        self.activityTimer?.invalidate()

        if self.isDragging {
            let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut)
            transition.updateAlpha(layer: self.dateIndicator.layer, alpha: 1.0)
            transition.updateAlpha(layer: self.lineIndicator.layer, alpha: 1.0)
        } else {
            self.activityTimer = SwiftSignalKit.Timer(timeout: 2.0, repeat: false, completion: { [weak self] in
                guard let strongSelf = self else {
                    return
                }
                let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut)
                transition.updateAlpha(layer: strongSelf.dateIndicator.layer, alpha: 0.0)
                transition.updateAlpha(layer: strongSelf.lineIndicator.layer, alpha: 0.0)

                strongSelf.dismissLineTooltip()
            }, queue: .mainQueue())
            self.activityTimer?.start()
        }
    }

    private func dismissLineTooltip() {
        if let lineTooltip = self.lineTooltip {
            self.lineTooltip = nil
            lineTooltip.layer.animateAlpha(from: lineTooltip.alpha, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak lineTooltip] _ in
                lineTooltip?.removeFromSuperview()
            })
        }
    }

    private func displayTooltipOnFirstScroll() {
        guard let displayTooltip = self.displayTooltip else {
            return
        }
        if self.displayedTooltip {
            return
        }
        self.displayedTooltip = true

        let lineTooltip = ComponentHostView<Empty>()
        self.lineTooltip = lineTooltip
        self.view.addSubview(lineTooltip)

        if let containerSize = self.containerSize {
            self.updateLineTooltip(containerSize: containerSize)
        }

        lineTooltip.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)

        let transition: ContainedViewLayoutTransition = .immediate
        transition.updateSublayerTransformOffset(layer: self.dateIndicator.layer, offset: CGPoint(x: -3.0, y: 0.0))

        displayTooltip.completed()

        //#if DEBUG
        //#else
        Queue.mainQueue().after(5.0, { [weak self] in
            self?.dismissLineTooltip()
        })
        //#endif
    }

    private func updateLineTooltip(containerSize: CGSize) {
        guard let displayTooltip = self.displayTooltip else {
            return
        }
        guard let lineTooltip = self.lineTooltip else {
            return
        }
        let lineTooltipSize = lineTooltip.update(
            transition: .immediate,
            component: AnyComponent(TooltipComponent(
                icon: displayTooltip.animation.flatMap { animation in
                    AnyComponent(ScrollingTooltipAnimationComponent())
                },
                content: AnyComponent(MultilineText(
                    text: displayTooltip.text,
                    font: Font.regular(13.0),
                    color: .white
                )),
                arrowLocation: self.lineIndicator.frame.insetBy(dx: -3.0, dy: -8.0)
            )),
            environment: {},
            containerSize: containerSize
        )
        lineTooltip.frame = CGRect(origin: CGPoint(), size: lineTooltipSize)
    }

    override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        if self.dateIndicator.alpha <= 0.01 {
            return nil
        }
        if self.dateIndicator.frame.offsetBy(dx: self.dateIndicatorContainer.frame.minX, dy: self.dateIndicatorContainer.frame.minY).contains(point) {
            return super.hitTest(point, with: event)
        }

        if self.lineIndicator.alpha <= 0.01 {
            return nil
        }
        if self.lineIndicator.frame.insetBy(dx: -4.0, dy: -2.0).contains(point) {
            return super.hitTest(point, with: event)
        }

        return nil
    }

    public func hideScroller() {
        let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut)
        transition.updateAlpha(layer: self.dateIndicator.layer, alpha: 0.0)
        transition.updateAlpha(layer: self.lineIndicator.layer, alpha: 0.0)

        self.dismissLineTooltip()
    }
}