import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import ViewControllerComponent
import ComponentDisplayAdapters
import TelegramPresentationData
import AccountContext
import TelegramCore
import MultilineTextComponent
import EmojiStatusComponent

private func processChartData(data: PieChartComponent.ChartData) -> PieChartComponent.ChartData {
    var data = data
    
    let minValue: Double = 0.01
    
    var totalSum: CGFloat = 0.0
    for i in 0 ..< data.items.count {
        if data.items[i].value > 0.00001 {
            data.items[i].value = max(data.items[i].value, minValue)
        }
        totalSum += data.items[i].value
    }
    
    var hasOneItem = false
    for i in 0 ..< data.items.count {
        if data.items[i].value != 0 && totalSum == data.items[i].value {
            data.items[i].value = 1.0
            hasOneItem = true
            break
        }
    }
    
    if !hasOneItem {
        if abs(totalSum - 1.0) > 0.0001 {
            let deltaValue = totalSum - 1.0
            
            var availableSum: Double = 0.0
            for i in 0 ..< data.items.count {
                let itemValue = data.items[i].value
                let availableItemValue = max(0.0, itemValue - minValue)
                if availableItemValue > 0.0 {
                    availableSum += availableItemValue
                }
            }
            totalSum = 0.0
            let itemFraction = deltaValue / availableSum
            for i in 0 ..< data.items.count {
                let itemValue = data.items[i].value
                let availableItemValue = max(0.0, itemValue - minValue)
                if availableItemValue > 0.0 {
                    let itemDelta = availableItemValue * itemFraction
                    data.items[i].value -= itemDelta
                }
                totalSum += data.items[i].value
            }
        }
        
        if totalSum > 0.0 && totalSum < 1.0 - 0.0001 {
            for i in 0 ..< data.items.count {
                data.items[i].value /= totalSum
            }
        }
    }
    
    return data
}

private let chartLabelFont = Font.with(size: 16.0, design: .round, weight: .semibold)

private final class ChartSelectionTooltip: Component {
    let theme: PresentationTheme
    let fractionText: String
    let title: String
    let sizeText: String
    
    init(
        theme: PresentationTheme,
        fractionText: String,
        title: String,
        sizeText: String
    ) {
        self.theme = theme
        self.fractionText = fractionText
        self.title = title
        self.sizeText = sizeText
    }
    
    static func ==(lhs: ChartSelectionTooltip, rhs: ChartSelectionTooltip) -> Bool {
        return true
    }
    
    final class View: UIView {
        private let backgroundView: BlurredBackgroundView
        private let title = ComponentView<Empty>()
        
        override init(frame: CGRect) {
            self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
            
            self.backgroundView.layer.shadowOpacity = 0.12
            self.backgroundView.layer.shadowColor = UIColor(white: 0.0, alpha: 1.0).cgColor
            self.backgroundView.layer.shadowOffset = CGSize(width: 0.0, height: 2.0)
            self.backgroundView.layer.shadowRadius = 8.0
            
            super.init(frame: frame)
            
            self.addSubview(self.backgroundView)
        }
        
        required init(coder: NSCoder) {
            preconditionFailure()
        }
        
        func update(component: ChartSelectionTooltip, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
            let sideInset: CGFloat = 10.0
            let height: CGFloat = 24.0
            
            let text = NSMutableAttributedString()
            text.append(NSAttributedString(string: component.fractionText + "  ", font: Font.semibold(12.0), textColor: component.theme.list.itemPrimaryTextColor))
            text.append(NSAttributedString(string: component.title + "  ", font: Font.regular(12.0), textColor: component.theme.list.itemPrimaryTextColor))
            text.append(NSAttributedString(string: component.sizeText, font: Font.semibold(12.0), textColor: component.theme.list.itemAccentColor))
            
            let titleSize = self.title.update(
                transition: transition,
                component: AnyComponent(MultilineTextComponent(
                    text: .plain(text)
                )),
                environment: {},
                containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
            )
            let titleFrame = CGRect(origin: CGPoint(x: sideInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize)
            if let titleView = self.title.view {
                if titleView.superview == nil {
                    self.addSubview(titleView)
                }
                transition.setFrame(view: titleView, frame: titleFrame)
            }
            
            let size = CGSize(width: sideInset * 2.0 + titleSize.width, height: height)
            
            transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size))
            self.backgroundView.updateColor(color: component.theme.list.plainBackgroundColor.withMultipliedAlpha(0.88), transition: .immediate)
            self.backgroundView.update(size: size, cornerRadius: 10.0, transition: transition.containedViewLayoutTransition)
            
            self.backgroundView.layer.shadowPath = UIBezierPath(roundedRect: self.backgroundView.bounds, cornerRadius: 10.0).cgPath
            
            return size
        }
    }
    
    func makeView() -> View {
        return View(frame: CGRect())
    }
    
    func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
        return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
    }
}

private final class ChartLabel: UIView {
    private let label: ImmediateTextView
    private var currentText: String?
    
    override init(frame: CGRect) {
        self.label = ImmediateTextView()
        
        super.init(frame: frame)
        
        self.addSubview(self.label)
    }
    
    required init(coder: NSCoder) {
        preconditionFailure()
    }
    
    func update(text: String) -> CGSize {
        if self.currentText == text {
            return self.label.bounds.size
        }
        
        var snapshotView: UIView?
        if self.currentText != nil {
            snapshotView = self.label.snapshotView(afterScreenUpdates: false)
            snapshotView?.frame = self.label.frame
        }
        
        self.currentText = text
        self.label.attributedText = NSAttributedString(string: text, font: chartLabelFont, textColor: .white)
        let size = self.label.updateLayout(CGSize(width: 100.0, height: 100.0))
        self.label.frame = CGRect(origin: CGPoint(x: floor(-size.width * 0.5), y: floor(-size.height * 0.5)), size: size)
        
        if let snapshotView {
            self.addSubview(snapshotView)
            snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
                snapshotView?.removeFromSuperview()
            })
            snapshotView.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false)
            self.label.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
            self.label.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2)
        }
        
        return size
    }
}

final class PieChartComponent: Component {
    struct ChartData: Equatable {
        struct Item: Equatable {
            var id: AnyHashable
            var displayValue: Double
            var displaySize: Int64
            var value: Double
            var color: UIColor
            var particle: String?
            var title: String
            var mergeable: Bool
            var mergeFactor: CGFloat
            
            init(id: AnyHashable, displayValue: Double, displaySize: Int64, value: Double, color: UIColor, particle: String?, title: String, mergeable: Bool, mergeFactor: CGFloat) {
                self.id = id
                self.displayValue = displayValue
                self.displaySize = displaySize
                self.value = value
                self.color = color
                self.particle = particle
                self.title = title
                self.mergeable = mergeable
                self.mergeFactor = mergeFactor
            }
        }
        
        var items: [Item]
        
        init(items: [Item]) {
            self.items = items
        }
    }
    
    let theme: PresentationTheme
    let strings: PresentationStrings
    let emptyColor: UIColor
    let chartData: ChartData
    
    init(
        theme: PresentationTheme,
        strings: PresentationStrings,
        emptyColor: UIColor,
        chartData: ChartData
    ) {
        self.theme = theme
        self.strings = strings
        self.emptyColor = emptyColor
        self.chartData = chartData
    }
    
    static func ==(lhs: PieChartComponent, rhs: PieChartComponent) -> Bool {
        if lhs.theme !== rhs.theme {
            return false
        }
        if lhs.strings !== rhs.strings {
            return false
        }
        if lhs.emptyColor != rhs.emptyColor {
            return false
        }
        if lhs.chartData != rhs.chartData {
            return false
        }
        return true
    }
    
    private struct CalculatedLabel {
        var image: UIImage
        var alpha: CGFloat
        var angle: CGFloat
        var radius: CGFloat
        var scale: CGFloat
        
        init(
            image: UIImage,
            alpha: CGFloat,
            angle: CGFloat,
            radius: CGFloat,
            scale: CGFloat
        ) {
            self.image = image
            self.alpha = alpha
            self.angle = angle
            self.radius = radius
            self.scale = scale
        }
        
        func interpolateTo(_ other: CalculatedLabel, amount: CGFloat) -> CalculatedLabel {
            return CalculatedLabel(
                image: other.image,
                alpha: self.alpha.interpolate(to: other.alpha, amount: amount),
                angle: self.angle.interpolate(to: other.angle, amount: amount),
                radius: self.radius.interpolate(to: other.radius, amount: amount),
                scale: self.scale.interpolate(to: other.scale, amount: amount)
            )
        }
    }
    
    private struct CalculatedSection {
        var id: AnyHashable
        var color: UIColor
        var particle: String?
        var title: String
        var innerAngle: Range<CGFloat>
        var outerAngle: Range<CGFloat>
        var innerRadius: CGFloat
        var outerRadius: CGFloat
        var label: CalculatedLabel?
        
        init(
            id: AnyHashable,
            color: UIColor,
            particle: String?,
            title: String,
            innerAngle: Range<CGFloat>,
            outerAngle: Range<CGFloat>,
            innerRadius: CGFloat,
            outerRadius: CGFloat,
            label: CalculatedLabel?
        ) {
            self.id = id
            self.color = color
            self.particle = particle
            self.title = title
            self.innerAngle = innerAngle
            self.outerAngle = outerAngle
            self.innerRadius = innerRadius
            self.outerRadius = outerRadius
            self.label = label
        }
    }
    
    private struct ItemAngleData {
        var angleValue: CGFloat
        var startAngle: CGFloat
        var endAngle: CGFloat
    }
    
    private struct CalculatedLayout {
        var size: CGSize
        var sections: [CalculatedSection]
        var isEmpty: Bool
        
        init(size: CGSize, sections: [CalculatedSection]) {
            self.size = size
            self.sections = sections
            self.isEmpty = sections.isEmpty
        }
        
        init(interpolating start: CalculatedLayout, to end: CalculatedLayout, progress: CGFloat, size: CGSize) {
            self.size = size
            self.sections = []
            self.isEmpty = end.isEmpty
            
            for i in 0 ..< end.sections.count {
                let right = end.sections[i]
                
                if i < start.sections.count {
                    let left = start.sections[i]
                    let innerAngle: Range<CGFloat> = left.innerAngle.lowerBound.interpolate(to: right.innerAngle.lowerBound, amount: progress) ..< left.innerAngle.upperBound.interpolate(to: right.innerAngle.upperBound, amount: progress)
                    let outerAngle: Range<CGFloat> = left.outerAngle.lowerBound.interpolate(to: right.outerAngle.lowerBound, amount: progress) ..< left.outerAngle.upperBound.interpolate(to: right.outerAngle.upperBound, amount: progress)
                    
                    var label: CalculatedLabel?
                    if let leftLabel = left.label, let rightLabel = right.label {
                        label = leftLabel.interpolateTo(rightLabel, amount: progress)
                    } else {
                        label = right.label
                    }
                    
                    self.sections.append(CalculatedSection(
                        id: right.id,
                        color: left.color.interpolateTo(right.color, fraction: progress) ?? right.color,
                        particle: right.particle,
                        title: right.title,
                        innerAngle: innerAngle,
                        outerAngle: outerAngle,
                        innerRadius: left.innerRadius.interpolate(to: right.innerRadius, amount: progress),
                        outerRadius: left.outerRadius.interpolate(to: right.outerRadius, amount: progress),
                        label: label
                    ))
                } else {
                    self.sections.append(right)
                }
            }
        }
        
        init(size: CGSize, items: [ChartData.Item], selectedKey: AnyHashable?, isEmpty: Bool, emptyColor: UIColor) {
            self.size = size
            self.sections = []
            self.isEmpty = isEmpty
            
            if items.isEmpty {
                return
            }
            
            let innerDiameter: CGFloat = isEmpty ? 90.0 : 100.0
            let spacing: CGFloat = isEmpty ? -0.5 : 2.0
            let innerAngleSpacing: CGFloat = spacing / (innerDiameter * 0.5)
            
            var angles: [Double] = []
            for i in 0 ..< items.count {
                let item = items[i]
                let angle = item.value * CGFloat.pi * 2.0
                angles.append(angle)
            }
            
            let diameter: CGFloat = isEmpty ? (innerDiameter + 6.0 * 2.0) : 200.0
            let reducedDiameter: CGFloat = floor(0.85 * diameter)
            
            var anglesData: [ItemAngleData] = []
            
            var startAngle: CGFloat = 0.0
            for i in 0 ..< items.count {
                let item = items[i]
                
                let itemOuterDiameter: CGFloat
                if let selectedKey {
                    if selectedKey == AnyHashable(item.id) {
                        itemOuterDiameter = diameter
                    } else {
                        itemOuterDiameter = reducedDiameter
                    }
                } else {
                    itemOuterDiameter = diameter
                }
                
                let angleSpacing: CGFloat = spacing / (itemOuterDiameter * 0.5)
                
                let angleValue: CGFloat = angles[i]
                
                let beforeSpacingFraction: CGFloat = 1.0
                let afterSpacingFraction: CGFloat = 1.0
                
                let itemInnerAngleSpacing: CGFloat
                let itemAngleSpacing: CGFloat
                if abs(angleValue - CGFloat.pi * 2.0) <= 0.0001 {
                    itemInnerAngleSpacing = 0.0
                    itemAngleSpacing = 0.0
                } else {
                    itemInnerAngleSpacing = innerAngleSpacing
                    itemAngleSpacing = angleSpacing
                }
                
                let innerStartAngle = startAngle + itemInnerAngleSpacing * 0.5
                let arcInnerStartAngle = startAngle + itemInnerAngleSpacing * 0.5 * beforeSpacingFraction
                
                var innerEndAngle = startAngle + angleValue - itemInnerAngleSpacing * 0.5
                innerEndAngle = max(innerEndAngle, innerStartAngle)
                var arcInnerEndAngle = startAngle + angleValue - itemInnerAngleSpacing * 0.5 * afterSpacingFraction
                arcInnerEndAngle = max(arcInnerEndAngle, arcInnerStartAngle)
                
                let outerStartAngle = startAngle + itemAngleSpacing * 0.5
                let arcOuterStartAngle = startAngle + itemAngleSpacing * 0.5 * beforeSpacingFraction
                var outerEndAngle = startAngle + angleValue - itemAngleSpacing * 0.5
                outerEndAngle = max(outerEndAngle, outerStartAngle)
                var arcOuterEndAngle = startAngle + angleValue - itemAngleSpacing * 0.5 * afterSpacingFraction
                arcOuterEndAngle = max(arcOuterEndAngle, arcOuterStartAngle)
                
                let itemColor: UIColor = isEmpty ? emptyColor : item.color
                
                self.sections.append(CalculatedSection(
                    id: item.id,
                    color: itemColor,
                    particle: item.particle,
                    title: item.title,
                    innerAngle: arcInnerStartAngle ..< arcInnerEndAngle,
                    outerAngle: arcOuterStartAngle ..< arcOuterEndAngle,
                    innerRadius: innerDiameter * 0.5,
                    outerRadius: itemOuterDiameter * 0.5,
                    label: nil
                ))
                
                startAngle += angleValue
                
                anglesData.append(ItemAngleData(angleValue: angleValue, startAngle: innerStartAngle, endAngle: innerEndAngle))
            }
            
            for i in 0 ..< items.count {
                let item = items[i]
                
                var isDimmedBySelection = false
                if let selectedKey {
                    if selectedKey == AnyHashable(item.id) {
                    } else {
                        isDimmedBySelection = true
                    }
                }
                
                self.updateLabel(
                    index: i,
                    displayValue: item.displayValue,
                    mergeFactor: item.mergeFactor,
                    innerAngle: self.sections[i].innerAngle,
                    outerAngle: self.sections[i].outerAngle,
                    innerRadius: self.sections[i].innerRadius,
                    outerRadius: self.sections[i].outerRadius,
                    isDimmedBySelection: isDimmedBySelection
                )
            }
        }
        
        private mutating func updateLabel(
            index: Int,
            displayValue: Double,
            mergeFactor: CGFloat,
            innerAngle: Range<CGFloat>,
            outerAngle: Range<CGFloat>,
            innerRadius: CGFloat,
            outerRadius: CGFloat,
            isDimmedBySelection: Bool
        ) {
            let normalAlpha: CGFloat = isDimmedBySelection ? 0.0 : 1.0
            
            let fractionValue: Double = floor(displayValue * 100.0 * 10.0) / 10.0
            let fractionString: String
            if displayValue == 0.0 {
                fractionString = ""
            } else if fractionValue < 0.1 {
                fractionString = "<0.1%"
            } else if abs(Double(Int(fractionValue)) - fractionValue) < 0.001 {
                fractionString = "\(Int(fractionValue))%"
            } else {
                fractionString = "\(fractionValue)%"
            }
            
            let labelString = NSAttributedString(string: fractionString, font: chartLabelFont, textColor: .white)
            let labelBounds = labelString.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: [.usesLineFragmentOrigin], context: nil)
            let labelSize = CGSize(width: ceil(labelBounds.width), height: ceil(labelBounds.height))
            guard let labelImage = generateImage(labelSize, rotatedContext: { size, context in
                context.clear(CGRect(origin: CGPoint(), size: size))
                UIGraphicsPushContext(context)
                labelString.draw(in: labelBounds)
                UIGraphicsPopContext()
            }) else {
                return
            }
            
            var resultLabel: CalculatedLabel?
            
            if innerAngle.upperBound - innerAngle.lowerBound >= 0.001 {
                for step in 0 ... 10 {
                    let stepFraction: CGFloat = CGFloat(step) / 10.0
                    let centerOffset: CGFloat = 0.5 * (1.0 - stepFraction) + 0.65 * stepFraction
                    
                    let midAngle: CGFloat = (innerAngle.lowerBound + innerAngle.upperBound) * 0.5
                    let centerDistance: CGFloat = (innerRadius + (outerRadius - innerRadius) * centerOffset)
                    
                    let relLabelCenter = CGPoint(
                        x: cos(midAngle) * centerDistance,
                        y: sin(midAngle) * centerDistance
                    )
                    
                    func lineCircleIntersection(_ center: CGPoint, _ p1: CGPoint, _ p2: CGPoint, _ r: CGFloat) -> CGFloat {
                        let dx: CGFloat = p2.x - p1.x
                        let dy: CGFloat = p2.y - p1.y
                        let dr: CGFloat = sqrt(dx * dx + dy * dy)
                        let D: CGFloat = p1.x * p2.y - p2.x * p1.y
                        
                        var minDistance: CGFloat = 10000.0
                        
                        for i in 0 ..< 2 {
                            let signFactor: CGFloat = i == 0 ? 1.0 : (-1.0)
                            let dysign: CGFloat = dy < 0.0 ? -1.0 : 1.0
                            let ix: CGFloat = (D * dy + signFactor * dysign * dx * sqrt(r * r * dr * dr - D * D)) / (dr * dr)
                            let iy: CGFloat = (-D * dx + signFactor * abs(dy) * sqrt(r * r * dr * dr - D * D)) / (dr * dr)
                            let distance: CGFloat = sqrt(pow(ix - center.x, 2.0) + pow(iy - center.y, 2.0))
                            minDistance = min(minDistance, distance)
                        }
                        
                        return minDistance
                    }
                    
                    func lineLineIntersection(_ p1: CGPoint, _ p2: CGPoint, _ p3: CGPoint, _ p4: CGPoint) -> CGFloat {
                        let x1 = p1.x
                        let y1 = p1.y
                        let x2 = p2.x
                        let y2 = p2.y
                        let x3 = p3.x
                        let y3 = p3.y
                        let x4 = p4.x
                        let y4 = p4.y
                        
                        let d: CGFloat = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4)
                        if abs(d) <= 0.00001 {
                            return 10000.0
                        }
                        
                        let px: CGFloat = ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / d
                        let py: CGFloat = ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / d
                        
                        let distance: CGFloat = sqrt(pow(px - p1.x, 2.0) + pow(py - p1.y, 2.0))
                        return distance
                    }
                    
                    let intersectionOuterTopRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), outerRadius)
                    let intersectionInnerTopRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), innerRadius)
                    let intersectionOuterBottomRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), outerRadius)
                    let intersectionInnerBottomRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), innerRadius)
                    
                    let horizontalInset: CGFloat = 2.0
                    let intersectionOuterLeft = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y), outerRadius) - horizontalInset
                    let intersectionInnerLeft = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y), innerRadius) - horizontalInset
                    
                    let intersectionLine1TopRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerAngle.lowerBound), y: sin(innerAngle.lowerBound)))
                    let intersectionLine1BottomRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerAngle.lowerBound), y: sin(innerAngle.lowerBound)))
                    let intersectionLine2TopRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerAngle.upperBound), y: sin(innerAngle.upperBound)))
                    let intersectionLine2BottomRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerAngle.upperBound), y: sin(innerAngle.upperBound)))
                    
                    var distances: [CGFloat] = [
                        intersectionOuterTopRight,
                        intersectionInnerTopRight,
                        intersectionOuterBottomRight,
                        intersectionInnerBottomRight,
                        intersectionOuterLeft,
                        intersectionInnerLeft
                    ]
                    
                    if innerAngle.upperBound - innerAngle.lowerBound < CGFloat.pi / 2.0 {
                        distances.append(contentsOf: [
                            intersectionLine1TopRight,
                            intersectionLine1BottomRight,
                            intersectionLine2TopRight,
                            intersectionLine2BottomRight
                        ] as [CGFloat])
                    }
                    
                    var minDistance: CGFloat = 1000.0
                    for distance in distances {
                        minDistance = min(minDistance, max(distance, 1.0))
                    }
                    
                    let diagonalAngle = atan2(labelSize.height, labelSize.width)
                    
                    let maxHalfWidth = cos(diagonalAngle) * minDistance
                    let maxHalfHeight = sin(diagonalAngle) * minDistance
                    
                    let maxSize = CGSize(width: maxHalfWidth * 2.0, height: maxHalfHeight * 2.0)
                    let finalSize = CGSize(width: min(labelSize.width, maxSize.width), height: min(labelSize.height, maxSize.height))
                    
                    let currentScale = finalSize.width / labelSize.width
                    
                    if currentScale >= 1.0 - 0.001 {
                        resultLabel = CalculatedLabel(
                            image: labelImage,
                            alpha: 1.0 * normalAlpha,
                            angle: midAngle,
                            radius: centerDistance,
                            scale: 1.0
                        )
                        break
                    }
                    if let resultLabel {
                        if resultLabel.scale > currentScale {
                            continue
                        }
                    }
                    resultLabel = CalculatedLabel(
                        image: labelImage,
                        alpha: (currentScale >= 0.4 ? 1.0 : 0.0) * normalAlpha,
                        angle: midAngle,
                        radius: centerDistance,
                        scale: currentScale
                    )
                }
            } else {
                let midAngle: CGFloat = (innerAngle.lowerBound + innerAngle.upperBound) * 0.5
                let centerDistance: CGFloat = (innerRadius + (outerRadius - innerRadius) * 0.5)
                
                resultLabel = CalculatedLabel(
                    image: labelImage,
                    alpha: 0.0,
                    angle: midAngle,
                    radius: centerDistance,
                    scale: 0.001
                )
            }
            
            if let resultLabel {
                self.sections[index].label = resultLabel
            }
        }
    }
    
    private struct Particle {
        var trackIndex: Int
        var position: CGPoint
        var scale: CGFloat
        var alpha: CGFloat
        var direction: CGPoint
        var velocity: CGFloat
        
        init(
            trackIndex: Int,
            position: CGPoint,
            scale: CGFloat,
            alpha: CGFloat,
            direction: CGPoint,
            velocity: CGFloat
        ) {
            self.trackIndex = trackIndex
            self.position = position
            self.scale = scale
            self.alpha = alpha
            self.direction = direction
            self.velocity = velocity
        }
        
        mutating func update(deltaTime: CGFloat) {
            var position = self.position
            position.x += self.direction.x * self.velocity * deltaTime
            position.y += self.direction.y * self.velocity * deltaTime
            self.position = position
        }
    }
    
    private final class ParticleSet {
        private let innerRadius: CGFloat
        private let maxRadius: CGFloat
        private(set) var particles: [Particle] = []
        
        init(innerRadius: CGFloat, maxRadius: CGFloat, preAdvance: Bool) {
            self.innerRadius = innerRadius
            self.maxRadius = maxRadius
            
            self.generateParticles(preAdvance: preAdvance)
        }
        
        private func generateParticles(preAdvance: Bool) {
            let maxDirections = 24
            
            if self.particles.count < maxDirections {
                var allTrackIndices: [Int] = Array(repeating: 0, count: maxDirections)
                for i in 0 ..< maxDirections {
                    allTrackIndices[i] = i
                }
                var takenIndexCount = 0
                for particle in self.particles {
                    allTrackIndices[particle.trackIndex] = -1
                    takenIndexCount += 1
                }
                var availableTrackIndices: [Int] = []
                availableTrackIndices.reserveCapacity(maxDirections - takenIndexCount)
                for index in allTrackIndices {
                    if index != -1 {
                        availableTrackIndices.append(index)
                    }
                }
                
                if !availableTrackIndices.isEmpty {
                    availableTrackIndices.shuffle()
                    
                    for takeIndex in availableTrackIndices {
                        let directionIndex = takeIndex
                        let angle = (CGFloat(directionIndex % maxDirections) / CGFloat(maxDirections)) * CGFloat.pi * 2.0
                        
                        let direction = CGPoint(x: cos(angle), y: sin(angle))
                        let velocity = CGFloat.random(in: 20.0 ..< 40.0)
                        let alpha = CGFloat.random(in: 0.1 ..< 0.4)
                        let scale = CGFloat.random(in: 0.5 ... 1.0) * 0.22
                        
                        var position = CGPoint(x: 100.0, y: 100.0)
                        var initialOffset: CGFloat = 0.4
                        if preAdvance {
                            initialOffset = CGFloat.random(in: initialOffset ... 1.0)
                        }
                        position.x += direction.x * initialOffset * 105.0
                        position.y += direction.y * initialOffset * 105.0
                        
                        let particle = Particle(
                            trackIndex: directionIndex,
                            position: position,
                            scale: scale,
                            alpha: alpha,
                            direction: direction,
                            velocity: velocity
                        )
                        self.particles.append(particle)
                    }
                }
            }
        }
        
        func update(deltaTime: CGFloat) {
            let size = CGSize(width: 200.0, height: 200.0)
            let radius = size.width * 0.5 + 10.0
            for i in (0 ..< self.particles.count).reversed() {
                self.particles[i].update(deltaTime: deltaTime)
                let position = self.particles[i].position
                
                let distance = sqrt(pow(position.x - size.width * 0.5, 2.0) + pow(position.y - size.height * 0.5, 2.0))
                if distance > radius {
                    self.particles.remove(at: i)
                }
            }
            
            self.generateParticles(preAdvance: false)
        }
    }
    
    private final class SectionLayer: SimpleLayer {
        private let maskLayer: SimpleShapeLayer
        private let gradientLayer: SimpleGradientLayer
        private let labelLayer: SimpleLayer
        
        private var currentLabelImage: UIImage?
        
        private var particleImage: UIImage?
        private var particleLayers: [SimpleLayer] = []
        
        init(particle: String?) {
            self.maskLayer = SimpleShapeLayer()
            self.maskLayer.fillColor = UIColor.white.cgColor
            
            self.gradientLayer = SimpleGradientLayer()
            self.gradientLayer.type = .radial
            self.gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5)
            self.gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
            
            self.labelLayer = SimpleLayer()
            
            super.init()
            
            self.mask = self.maskLayer
            self.addSublayer(self.gradientLayer)
            self.addSublayer(self.labelLayer)
            
            if let particle {
                self.particleImage = UIImage(bundleImageName: particle)?.precomposed()
            }
        }
        
        override init(layer: Any) {
            self.maskLayer = SimpleShapeLayer()
            self.gradientLayer = SimpleGradientLayer()
            self.labelLayer = SimpleLayer()
            
            super.init(layer: layer)
        }
        
        required init(coder: NSCoder) {
            preconditionFailure()
        }
        
        func isPointOnGraph(point: CGPoint) -> Bool {
            if let path = self.maskLayer.path {
                return path.contains(point)
            }
            return false
        }
        
        func tooltipLocation() -> CGPoint {
            return self.labelLayer.position
        }
        
        func update(size: CGSize, section: CalculatedSection) {
            self.maskLayer.frame = CGRect(origin: CGPoint(), size: size)
            self.gradientLayer.frame = CGRect(origin: CGPoint(), size: size)
            
            let normalColor = section.color.cgColor
            let darkerColor = section.color.withMultipliedBrightnessBy(0.96).cgColor
            let colors: [CGColor] = [
                darkerColor,
                normalColor,
                normalColor,
                normalColor,
                darkerColor
            ]
            self.gradientLayer.colors = colors
            
            let locations: [CGFloat] = [
                0.0,
                0.3,
                0.5,
                0.7,
                1.0
            ]
            self.gradientLayer.locations = locations.map { location in
                let location = location * 0.5 + 0.5
                return location as NSNumber
            }
            
            let path = CGMutablePath()
            path.addArc(center: CGPoint(x: size.width * 0.5, y: size.height * 0.5), radius: section.innerRadius, startAngle: section.innerAngle.upperBound, endAngle: section.innerAngle.lowerBound, clockwise: true)
            path.addArc(center: CGPoint(x: size.width * 0.5, y: size.height * 0.5), radius: section.outerRadius, startAngle: section.outerAngle.lowerBound, endAngle: section.outerAngle.upperBound, clockwise: false)
            self.maskLayer.path = path
            
            if let label = section.label {
                if self.currentLabelImage !== label.image {
                    self.currentLabelImage = label.image
                    self.labelLayer.contents = label.image.cgImage
                }
                
                let position = CGPoint(x: size.width * 0.5 + cos(label.angle) * label.radius, y: size.height * 0.5 + sin(label.angle) * label.radius)
                let labelSize = CGSize(width: label.image.size.width * label.scale, height: label.image.size.height * label.scale)
                let labelFrame = CGRect(origin: CGPoint(x: position.x - labelSize.width * 0.5, y: position.y - labelSize.height * 0.5), size: labelSize)
                self.labelLayer.frame = labelFrame
                self.labelLayer.opacity = Float(label.alpha)
            } else {
                self.currentLabelImage = nil
                self.labelLayer.contents = nil
            }
        }
        
        func updateParticles(particleSet: ParticleSet, alpha: CGFloat) {
            guard let particleImage = self.particleImage else {
                return
            }
            for i in 0 ..< particleSet.particles.count {
                let particle = particleSet.particles[i]
                
                let particleLayer: SimpleLayer
                if i < self.particleLayers.count {
                    particleLayer = self.particleLayers[i]
                    particleLayer.isHidden = false
                } else {
                    particleLayer = SimpleLayer()
                    particleLayer.contents = particleImage.cgImage
                    particleLayer.bounds = CGRect(origin: CGPoint(), size: particleImage.size)
                    self.particleLayers.append(particleLayer)
                    self.insertSublayer(particleLayer, above: self.gradientLayer)
                }
                
                particleLayer.position = particle.position
                particleLayer.transform = CATransform3DMakeScale(particle.scale, particle.scale, 1.0)
                particleLayer.opacity = Float(particle.alpha * alpha)
            }
            if particleSet.particles.count < self.particleLayers.count {
                for i in particleSet.particles.count ..< self.particleLayers.count {
                    self.particleLayers[i].isHidden = true
                }
            }
        }
    }
    
    private final class DoneLayer: SimpleLayer {
        private let particleColor: UIColor
        private let maskShapeLayer: CAShapeLayer
        private var particleImage: UIImage?
        private var particleSet: ParticleSet?
        private var particleLayers: [SimpleLayer] = []
        
        init(particleColor: UIColor) {
            self.particleColor = particleColor
            
            self.maskShapeLayer = CAShapeLayer()
            self.maskShapeLayer.fillColor = UIColor.black.cgColor
            self.maskShapeLayer.fillRule = .evenOdd
            
            super.init()
            
            self.particleImage = UIImage(bundleImageName: "Settings/Storage/ParticleStar")?.precomposed()
            
            let path = CGMutablePath()
            
            path.addRect(CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 200.0, height: 200.0)))
            path.addEllipse(in: CGRect(origin: CGPoint(x: floor((200.0 - 102.0) * 0.5), y: floor((200.0 - 102.0) * 0.5)), size: CGSize(width: 102.0, height: 102.0)))
            
            self.maskShapeLayer.path = path
            self.mask = self.maskShapeLayer
            
            self.particleSet = ParticleSet(innerRadius: 45.0, maxRadius: 100.0, preAdvance: true)
        }
        
        override init(layer: Any) {
            self.particleColor = .white
            self.maskShapeLayer = CAShapeLayer()
            
            super.init(layer: layer)
        }
        
        required init(coder: NSCoder) {
            preconditionFailure()
        }
        
        func updateParticles(deltaTime: CGFloat) {
            guard let particleSet = self.particleSet else {
                return
            }
            particleSet.update(deltaTime: deltaTime)
            
            let size = CGSize(width: 200.0, height: 200.0)
            
            guard let particleImage = self.particleImage else {
                return
            }
            for i in 0 ..< particleSet.particles.count {
                let particle = particleSet.particles[i]
                
                let particleLayer: SimpleLayer
                if i < self.particleLayers.count {
                    particleLayer = self.particleLayers[i]
                    particleLayer.isHidden = false
                } else {
                    particleLayer = SimpleLayer()
                    particleLayer.contents = particleImage.cgImage
                    particleLayer.bounds = CGRect(origin: CGPoint(), size: particleImage.size)
                    self.particleLayers.append(particleLayer)
                    self.addSublayer(particleLayer)
                    
                    particleLayer.layerTintColor = self.particleColor.cgColor
                }
                
                particleLayer.position = particle.position
                particleLayer.transform = CATransform3DMakeScale(particle.scale * 1.2, particle.scale * 1.2, 1.0)
                
                let distance = sqrt(pow(particle.position.x - size.width * 0.5, 2.0) + pow(particle.position.y - size.height * 0.5, 2.0))
                var mulAlpha: CGFloat = 1.0
                let outerDistanceNorm: CGFloat = 20.0
                if distance > 100.0 - outerDistanceNorm {
                    let outerDistanceFactor: CGFloat = (100.0 - distance) / outerDistanceNorm
                    let alphaFactor: CGFloat = max(0.0, min(1.0, outerDistanceFactor))
                    mulAlpha = alphaFactor
                }
                
                particleLayer.opacity = Float(particle.alpha * mulAlpha)
            }
            if particleSet.particles.count < self.particleLayers.count {
                for i in particleSet.particles.count ..< self.particleLayers.count {
                    self.particleLayers[i].isHidden = true
                }
            }
        }
    }
    
    private final class ChartDataView: UIView {
        private(set) var theme: PresentationTheme?
        private(set) var data: ChartData?
        private var emptyColor: UIColor?
        private(set) var selectedKey: AnyHashable?
        
        private var currentAnimation: (start: CalculatedLayout, startTime: Double, duration: Double)?
        private var currentLayout: CalculatedLayout?
        private var animator: DisplayLinkAnimator?
        
        private var displayLink: SharedDisplayLinkDriver.Link?
        
        private var sectionLayers: [AnyHashable: SectionLayer] = [:]
        private let particleSet: ParticleSet
        private var doneLayer: DoneLayer?
        
        override init(frame: CGRect) {
            self.particleSet = ParticleSet(innerRadius: 50.0, maxRadius: 100.0, preAdvance: true)
            
            super.init(frame: frame)
            
            self.backgroundColor = nil
            self.isOpaque = false
            
            self.displayLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] delta in
                self?.update(deltaTime: CGFloat(delta))
            })
        }
        
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
        
        deinit {
            self.animator?.invalidate()
        }
        
        func sectionKey(at point: CGPoint) -> AnyHashable? {
            for (id, itemLayer) in self.sectionLayers {
                if itemLayer.isPointOnGraph(point: point) {
                    return id
                }
            }
            return nil
        }
        
        func tooltipLocation(forKey key: AnyHashable) -> CGPoint? {
            for (id, itemLayer) in self.sectionLayers {
                if id == key {
                    return itemLayer.tooltipLocation()
                }
            }
            return nil
        }
        
        func setItems(theme: PresentationTheme, emptyColor: UIColor, data: ChartData, selectedKey: AnyHashable?, animated: Bool) {
            self.emptyColor = emptyColor
            
            let data = processChartData(data: data)
            
            if self.theme !== theme || self.data != data || self.selectedKey != selectedKey {
                self.theme = theme
                self.selectedKey = selectedKey
                let previousData = self.data
                
                if animated, let previous = self.currentLayout {
                    var initialState = previous
                    if let currentAnimation = self.currentAnimation {
                        let currentProgress: Double = max(0.0, min(1.0, (CACurrentMediaTime() - currentAnimation.startTime) / currentAnimation.duration))
                        let mappedProgress = listViewAnimationCurveSystem(CGFloat(currentProgress))
                        initialState = CalculatedLayout(interpolating: currentAnimation.start, to: previous, progress: mappedProgress, size: previous.size)
                    }
                    
                    let targetLayout: CalculatedLayout
                    if let previousData = previousData, data.items.isEmpty {
                        targetLayout = CalculatedLayout(
                            size: CGSize(width: 200.0, height: 200.0),
                            items: previousData.items,
                            selectedKey: self.selectedKey,
                            isEmpty: true,
                            emptyColor: emptyColor
                        )
                    } else {
                        targetLayout = CalculatedLayout(
                            size: CGSize(width: 200.0, height: 200.0),
                            items: data.items,
                            selectedKey: self.selectedKey,
                            isEmpty: false,
                            emptyColor: emptyColor
                        )
                    }
                    
                    self.currentLayout = targetLayout
                    self.currentAnimation = (initialState, CACurrentMediaTime(), 0.4)
                } else {
                    if data.items.isEmpty {
                        self.currentLayout = CalculatedLayout(
                            size: CGSize(width: 200.0, height: 200.0),
                            items: [.init(id: AnyHashable(StorageUsageScreenComponent.Category.other), displayValue: 0.0, displaySize: 0, value: 1.0, color: .green, particle: "Settings/Storage/ParticleOther", title: "", mergeable: false, mergeFactor: 1.0)],
                            selectedKey: self.selectedKey,
                            isEmpty: true,
                            emptyColor: emptyColor
                        )
                    } else {
                        self.currentLayout = CalculatedLayout(
                            size: CGSize(width: 200.0, height: 200.0),
                            items: data.items,
                            selectedKey: self.selectedKey,
                            isEmpty: data.items.isEmpty,
                            emptyColor: emptyColor
                        )
                    }
                }
                
                self.data = data
                
                self.update(deltaTime: 0.0)
            }
        }
        
        private func update(deltaTime: CGFloat) {
            self.particleSet.update(deltaTime: deltaTime)
            
            var validIds: [AnyHashable] = []
            if let currentLayout = self.currentLayout, let emptyColor = self.emptyColor {
                var effectiveLayout = currentLayout
                var verticalOffset: CGFloat = 0.0
                var particleAlpha: CGFloat = 1.0
                var rotationAngle: CGFloat = 0.0
                let emptyRotationAngle: CGFloat = CGFloat.pi
                let emptyVerticalOffset: CGFloat = (92.0 - 200.0) * 0.5
                if let currentAnimation = self.currentAnimation {
                    let currentProgress: Double = max(0.0, min(1.0, (CACurrentMediaTime() - currentAnimation.startTime) / currentAnimation.duration))
                    let mappedProgress = listViewAnimationCurveSystem(CGFloat(currentProgress))
                    
                    effectiveLayout = CalculatedLayout(interpolating: currentAnimation.start, to: currentLayout, progress: mappedProgress, size: currentLayout.size)
                    
                    let fromVerticalOffset: CGFloat
                    let fromRotationAngle: CGFloat
                    if currentAnimation.start.isEmpty {
                        fromVerticalOffset = emptyVerticalOffset
                        fromRotationAngle = emptyRotationAngle
                    } else {
                        fromVerticalOffset = 0.0
                        fromRotationAngle = 0.0
                    }
                    let toVerticalOffset: CGFloat
                    let toRotationAngle: CGFloat
                    if currentLayout.isEmpty {
                        toVerticalOffset = emptyVerticalOffset
                        toRotationAngle = emptyRotationAngle
                    } else {
                        toVerticalOffset = 0.0
                        toRotationAngle = 0.0
                    }
                    
                    verticalOffset = (1.0 - mappedProgress) * fromVerticalOffset + mappedProgress * toVerticalOffset
                    rotationAngle = (1.0 - mappedProgress) * fromRotationAngle + mappedProgress * toRotationAngle
                    
                    if currentLayout.isEmpty {
                        particleAlpha = 1.0 - mappedProgress
                    }
                    
                    if currentProgress >= 1.0 - CGFloat.ulpOfOne {
                        self.currentAnimation = nil
                    }
                } else {
                    if currentLayout.isEmpty {
                        verticalOffset = emptyVerticalOffset
                        particleAlpha = 0.0
                        rotationAngle = emptyRotationAngle
                    }
                }
                
                if currentLayout.isEmpty {
                    let doneLayer: DoneLayer
                    if let current = self.doneLayer {
                        doneLayer = current
                    } else {
                        doneLayer = DoneLayer(particleColor: emptyColor)
                        self.doneLayer = doneLayer
                        self.layer.insertSublayer(doneLayer, at: 0)
                    }
                    doneLayer.updateParticles(deltaTime: deltaTime)
                    doneLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: CGSize(width: 200.0, height: 200.0))
                    doneLayer.opacity = Float(1.0 - particleAlpha)
                } else {
                    if let doneLayer = self.doneLayer {
                        self.doneLayer = nil
                        doneLayer.removeFromSuperlayer()
                    }
                }
                
                for section in effectiveLayout.sections {
                    validIds.append(section.id)
                    
                    let sectionLayer: SectionLayer
                    if let current = self.sectionLayers[section.id] {
                        sectionLayer = current
                    } else {
                        sectionLayer = SectionLayer(particle: section.particle)
                        self.sectionLayers[section.id] = sectionLayer
                        self.layer.addSublayer(sectionLayer)
                    }
                    
                    let sectionLayerFrame = CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: CGSize(width: 200.0, height: 200.0))
                    sectionLayer.position = sectionLayerFrame.center
                    sectionLayer.bounds = CGRect(origin: CGPoint(), size: sectionLayerFrame.size)
                    sectionLayer.transform = CATransform3DMakeRotation(rotationAngle, 0.0, 0.0, 1.0)
                    sectionLayer.update(size: sectionLayer.bounds.size, section: section)
                    sectionLayer.updateParticles(particleSet: self.particleSet, alpha: particleAlpha)
                }
            }
            
            var removeIds: [AnyHashable] = []
            for (id, sectionLayer) in self.sectionLayers {
                if !validIds.contains(id) {
                    removeIds.append(id)
                    sectionLayer.removeFromSuperlayer()
                }
            }
            for id in removeIds {
                self.sectionLayers.removeValue(forKey: id)
            }
        }
    }
    
    class View: UIView {
        private let dataView: ChartDataView
        private var tooltip: (key: AnyHashable, value: ComponentView<Empty>)?
        
        var selectedKey: AnyHashable?
        
        private var component: PieChartComponent?
        private weak var state: EmptyComponentState?
        
        override init(frame: CGRect) {
            self.dataView = ChartDataView()
            
            super.init(frame: frame)
            
            self.addSubview(self.dataView)
            
            self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
        }
        
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
        
        @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
            if case .ended = recognizer.state {
                let point = recognizer.location(in: self.dataView)
                if let key = self.dataView.sectionKey(at: point), key != AnyHashable("empty") {
                    if self.selectedKey == key {
                        self.selectedKey = nil
                    } else {
                        self.selectedKey = key
                    }
                } else {
                    self.selectedKey = nil
                }
                self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring)))
            }
        }
        
        func update(component: PieChartComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
            let dataUpdated = self.component?.chartData != component.chartData
            
            self.state = state
            self.component = component
            
            if dataUpdated {
                self.selectedKey = nil
            }
            
            transition.setFrame(view: self.dataView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - 200.0) / 2.0), y: 0.0), size: CGSize(width: 200.0, height: 200.0)))
            self.dataView.setItems(theme: component.theme, emptyColor: component.emptyColor, data: component.chartData, selectedKey: self.selectedKey, animated: !transition.animation.isImmediate)
            
            if let selectedKey = self.selectedKey, let item = component.chartData.items.first(where: { $0.id == selectedKey }) {
                let tooltip: ComponentView<Empty>
                var tooltipTransition = transition
                var animateIn = false
                if let current = self.tooltip, current.key == AnyHashable(selectedKey) {
                    tooltip = current.value
                } else if let current = self.tooltip {
                    if let tooltipView = current.value.view {
                        transition.setAlpha(view: tooltipView, alpha: 0.0, completion: { [weak tooltipView] _ in
                            tooltipView?.removeFromSuperview()
                        })
                    }
                    tooltipTransition = .immediate
                    animateIn = true
                    tooltip = ComponentView()
                    self.tooltip = (selectedKey, tooltip)
                } else {
                    tooltipTransition = .immediate
                    animateIn = true
                    tooltip = ComponentView()
                    self.tooltip = (selectedKey, tooltip)
                }
                
                let fractionValue: Double = floor(item.displayValue * 100.0 * 10.0) / 10.0
                let fractionString: String
                if fractionValue < 0.1 {
                    fractionString = "<0.1%"
                } else if abs(Double(Int(fractionValue)) - fractionValue) < 0.001 {
                    fractionString = "\(Int(fractionValue))%"
                } else {
                    fractionString = "\(fractionValue)%"
                }
                
                let tooltipSize = tooltip.update(
                    transition: tooltipTransition,
                    component: AnyComponent(ChartSelectionTooltip(
                        theme: component.theme,
                        fractionText: fractionString,
                        title: item.title,
                        sizeText: dataSizeString(Int(item.displaySize), formatting: DataSizeStringFormatting(strings: component.strings, decimalSeparator: "."))
                    )),
                    environment: {},
                    containerSize: availableSize
                )
                
                if let relativeTooltipLocation = self.dataView.tooltipLocation(forKey: selectedKey) {
                    let tooltipLocation = relativeTooltipLocation.offsetBy(dx: self.dataView.frame.minX, dy: self.dataView.frame.minY)
                    let tooltipFrame = CGRect(origin: CGPoint(x: floor(tooltipLocation.x - tooltipSize.width / 2.0), y: tooltipLocation.y - 16.0 - tooltipSize.height), size: tooltipSize)
                    
                    if let tooltipView = tooltip.view {
                        if tooltipView.superview == nil {
                            self.addSubview(tooltipView)
                        }
                        tooltipTransition.setFrame(view: tooltipView, frame: tooltipFrame)
                        if animateIn {
                            transition.animateAlpha(view: tooltipView, from: 0.0, to: 1.0)
                            transition.animateScale(view: tooltipView, from: 0.8, to: 1.0)
                        }
                    }
                }
            } else {
                if let tooltip = self.tooltip {
                    self.tooltip = nil
                    if let tooltipView = tooltip.value.view {
                        transition.setAlpha(view: tooltipView, alpha: 0.0, completion: { [weak tooltipView] _ in
                            tooltipView?.removeFromSuperview()
                        })
                        transition.setScale(view: tooltipView, scale: 0.8)
                    }
                }
            }
            
            return CGSize(width: availableSize.width, height: 200.0)
        }
    }
    
    func makeView() -> View {
        return View(frame: CGRect())
    }
    
    func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
        return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
    }
}