import Foundation
import UIKit
import ComponentFlow
import Display
import AccountContext
import SwiftSignalKit
import AVKit
import TelegramCore
import Postbox
import ShareController
import UndoUI
import TelegramPresentationData
import PresentationDataUtils
import LottieAnimationComponent
import ContextUI
import ViewControllerComponent
import BundleIconComponent
import CreateExternalMediaStreamScreen
import HierarchyTrackingLayer
import UndoPanelComponent
import AvatarNode

public final class MediaStreamComponent: CombinedComponent {
    struct OriginInfo: Equatable {
        var title: String
        var memberCount: Int
    }
    
    public typealias EnvironmentType = ViewControllerComponentContainer.Environment
    
    public let call: PresentationGroupCallImpl
    
    public init(call: PresentationGroupCallImpl) {
        self.call = call
    }
    
    public static func ==(lhs: MediaStreamComponent, rhs: MediaStreamComponent) -> Bool {
        if lhs.call !== rhs.call {
            return false
        }
        
        return true
    }
    
    public final class State: ComponentState {
        private let call: PresentationGroupCallImpl
        
        private(set) var hasVideo: Bool = false
        private var stateDisposable: Disposable?
        private var infoDisposable: Disposable?
        
        private(set) var originInfo: OriginInfo?
        
        private(set) var displayUI: Bool = true
        var dismissOffset: CGFloat = 0.0
        var initialOffset: CGFloat = 0.0
        var storedIsFullscreen: Bool?
        var isFullscreen: Bool = false
        var videoSize: CGSize?
        var prevFullscreenOrientation: UIDeviceOrientation?
        
        private(set) var canManageCall: Bool = false
        let isPictureInPictureSupported: Bool
        
        private(set) var callTitle: String?
        private(set) var recordingStartTimestamp: Int32?
        
        private(set) var peerTitle: String = ""
        private(set) var chatPeer: Peer?
        
        private(set) var isVisibleInHierarchy: Bool = false
        private var isVisibleInHierarchyDisposable: Disposable?
        
        private var scheduledDismissUITimer: SwiftSignalKit.Timer?
        var videoStalled: Bool = true
        
        var videoIsPlayable: Bool {
            !videoStalled && hasVideo
        }
//        var wantsPiP: Bool = false
        
        let deactivatePictureInPictureIfVisible = StoredActionSlot(Void.self)
        
        private let infoThrottler = Throttler<Int>.init(duration: 5, queue: .main)

        init(call: PresentationGroupCallImpl) {
            self.call = call
            
            if #available(iOSApplicationExtension 15.0, iOS 15.0, *), AVPictureInPictureController.isPictureInPictureSupported() {
                self.isPictureInPictureSupported = true
            } else {
                self.isPictureInPictureSupported = AVPictureInPictureController.isPictureInPictureSupported()
            }
            
            super.init()
            
            self.stateDisposable = (call.state
            |> map { state -> Bool in
                switch state.networkState {
                case .connected:
                    return true
                default:
                    return false
                }
            }
            |> filter { $0 }
            |> take(1)).start(next: { [weak self] _ in
                guard let strongSelf = self else {
                    return
                }
                strongSelf.hasVideo = true
                strongSelf.updated(transition: .immediate)
            })
            
            let callPeer = call.accountContext.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: call.peerId))
            
            self.infoDisposable = (combineLatest(queue: .mainQueue(), call.state, call.members, callPeer)
            |> deliverOnMainQueue).start(next: { [weak self] state, members, callPeer in
                guard let strongSelf = self, let members = members, let callPeer = callPeer else {
                    return
                }
                
                var updated = false
//                 TODO: remove debug timer
//                Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in
                var shouldReplaceNoViewersWithOne: Bool { true }
                let membersCount = members.totalCount // Int.random(in: 0..<10000000) //
                strongSelf.infoThrottler.publish(shouldReplaceNoViewersWithOne ? max(membersCount, 1) : membersCount) { [weak strongSelf] latestCount in
                        let _ = members.totalCount
                        guard let strongSelf = strongSelf else { return }
                        var updated = false
                        let originInfo = OriginInfo(title: callPeer.debugDisplayTitle, memberCount: latestCount)
                        if strongSelf.originInfo != originInfo {
                            strongSelf.originInfo = originInfo
                            updated = true
                        }    
                        if updated {
                            strongSelf.updated(transition: .immediate)
                        }
                    }
//                }.fire()
                if state.canManageCall != strongSelf.canManageCall {
                    strongSelf.canManageCall = state.canManageCall
                    updated = true
                }
                if strongSelf.peerTitle != callPeer.debugDisplayTitle {
                    strongSelf.peerTitle = callPeer.debugDisplayTitle
                    updated = true
                }
                strongSelf.chatPeer = callPeer._asPeer()
                
                if strongSelf.callTitle != state.title {
                    strongSelf.callTitle = state.title
                    updated = true
                }
                
                if strongSelf.recordingStartTimestamp != state.recordingStartTimestamp {
                    strongSelf.recordingStartTimestamp = state.recordingStartTimestamp
                    updated = true
                }
                
                if updated {
                    strongSelf.updated(transition: .immediate)
                }
            })
            
            self.isVisibleInHierarchyDisposable = (call.accountContext.sharedContext.applicationBindings.applicationInForeground
            |> deliverOnMainQueue).start(next: { [weak self] inForeground in
                guard let strongSelf = self else {
                    return
                }
                if strongSelf.isVisibleInHierarchy != inForeground {
                    strongSelf.isVisibleInHierarchy = inForeground
                    strongSelf.updated(transition: .immediate)
                    
                    if inForeground {
                        Queue.mainQueue().after(0.5, {
                            guard let strongSelf = self, strongSelf.isVisibleInHierarchy else {
                                return
                            }
                            
                            strongSelf.deactivatePictureInPictureIfVisible.invoke(Void())
                        })
                    } else {
                        // MARK: TODO: fullscreen ui toggle
                    }
                }
            })
        }
        
        deinit {
            self.stateDisposable?.dispose()
            self.infoDisposable?.dispose()
            self.isVisibleInHierarchyDisposable?.dispose()
        }
        
        func toggleDisplayUI() {
            self.displayUI = !self.displayUI
            self.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .easeInOut)))
        }
        
        func cancelScheduledDismissUI() {
            self.scheduledDismissUITimer?.invalidate()
            self.scheduledDismissUITimer = nil
        }
        
        func scheduleDismissUI() {
            if self.scheduledDismissUITimer == nil {
                self.scheduledDismissUITimer = SwiftSignalKit.Timer(timeout: 3.0, repeat: false, completion: { [weak self] in
                    guard let strongSelf = self else {
                        return
                    }
                    strongSelf.scheduledDismissUITimer = nil
                    if strongSelf.displayUI {
                        strongSelf.toggleDisplayUI()
                    }
                }, queue: .mainQueue())
                self.scheduledDismissUITimer?.start()
            }
        }
        
        func updateDismissOffset(value: CGFloat, interactive: Bool) {
            self.dismissOffset = value
            if interactive {
                self.updated(transition: .immediate)
            } else {
                self.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut)))
            }
        }
    }
    
    public func makeState() -> State {
        return State(call: self.call)
    }
    
    class Local {
        let background = Child(Rectangle.self)
        let dismissTapComponent = Child(Rectangle.self)
        let video = Child(MediaStreamVideoComponent.self)
        let sheet = Child(StreamSheetComponent.self)
        let topItem = Child(environment: Empty.self)
        let fullscreenBottomItem = Child(environment: Empty.self)
        let buttonsRow = Child(environment: Empty.self)
        
        let activatePictureInPicture = StoredActionSlot(Action<Void>.self)
        let deactivatePictureInPicture = StoredActionSlot(Void.self)
        let moreButtonTag = GenericComponentViewTag()
        let moreAnimationTag = GenericComponentViewTag()
    }
    
    public static var body: Body {
        let local = Local()
        
        return { context in
            _body(context, local) // { context in
        }
    }
    
    private static func _body(_ context: CombinedComponentContext<MediaStreamComponent>, _ local: Local) -> CGSize {
        let background = local.background
        let dismissTapComponent = local.dismissTapComponent
        let video = local.video
        let sheet = local.sheet
        let topItem = local.topItem
        let fullscreenBottomItem = local.fullscreenBottomItem
        let buttonsRow = local.buttonsRow
        
        let activatePictureInPicture = local.activatePictureInPicture
        let deactivatePictureInPicture = local.deactivatePictureInPicture
        let moreButtonTag = local.moreButtonTag
        let moreAnimationTag = local.moreAnimationTag
        
        let makeBody: () -> CGSize = { [weak local] in
            let canEnforceOrientation = UIDevice.current.model != "iPad"
            var forceFullScreenInLandscape: Bool { canEnforceOrientation && true }
            let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
            if environment.isVisible {
            } else {
                context.state.dismissOffset = 0.0
            }
            
            let background = background.update(
                component: Rectangle(color: .black.withAlphaComponent(0.0)),
                availableSize: context.availableSize,
                transition: context.transition
            )
            
            let call = context.component.call
            let state = context.state
            let controller = environment.controller
            
            context.state.deactivatePictureInPictureIfVisible.connect { [weak state] in
                guard let controller = controller(), controller.view.window != nil else {
                    return
                }
                
                guard let state else {
                    return
                }
                state.updated(transition: .easeInOut(duration: 3))
                
                guard let local else {
                    return
                }
                local.deactivatePictureInPicture.invoke(Void())
            }
            let isFullscreen: Bool
            let isLandscape = context.availableSize.width > context.availableSize.height
            
            // Always fullscreen in landscape
            if forceFullScreenInLandscape && isLandscape && !state.isFullscreen {
                state.isFullscreen = true
                isFullscreen = true
            } else if !isLandscape && state.isFullscreen && canEnforceOrientation {
                state.prevFullscreenOrientation = nil
                state.isFullscreen = false
                isFullscreen = false
            } else {
                isFullscreen = state.isFullscreen
            }
            
            let videoInset: CGFloat
            if !isFullscreen {
                videoInset = 16.0
            } else {
                videoInset = 0.0
            }
            
            let videoHeight: CGFloat = forceFullScreenInLandscape
            ? (context.availableSize.width - videoInset * 2) / 16 * 9
            : context.state.videoSize?.height ?? (min(context.availableSize.width, context.availableSize.height) - videoInset * 2) / 16.0 * 9.0
            let bottomPadding = 32.0 + environment.safeInsets.bottom
            let requiredSheetHeight: CGFloat = isFullscreen
            ? context.availableSize.height
            : (44.0 + videoHeight + 40.0 + 69.0 + 16.0 + 32.0 + 70.0 + bottomPadding + 8.0)
            
            let safeAreaTopInView: CGFloat
            if #available(iOS 16.0, *) {
                safeAreaTopInView = context.view.window.flatMap { $0.convert(CGPoint(x: 0, y: $0.safeAreaInsets.top), to: context.view).y } ?? 0
            } else {
                safeAreaTopInView = context.view.safeAreaInsets.top
            }
            
            let isFullyDragged = context.availableSize.height - requiredSheetHeight + state.dismissOffset - safeAreaTopInView < 30.0
            
            var dragOffset = context.state.dismissOffset
            if isFullyDragged {
                dragOffset = max(context.state.dismissOffset, requiredSheetHeight - context.availableSize.height + safeAreaTopInView)
            }
            
            let dismissTapAreaHeight = isFullscreen ? 0 : (context.availableSize.height - requiredSheetHeight + dragOffset)
            let dismissTapComponent = dismissTapComponent.update(
                component: Rectangle(color: .red.withAlphaComponent(0)),
                availableSize: CGSize(width: context.availableSize.width, height: dismissTapAreaHeight),
                transition: context.transition
            )
            
            let video = video.update(
                component: MediaStreamVideoComponent(
                    call: context.component.call,
                    hasVideo: context.state.hasVideo,
                    isVisible: environment.isVisible && context.state.isVisibleInHierarchy,
                    isAdmin: context.state.canManageCall,
                    peerTitle: context.state.peerTitle,
                    isFullscreen: isFullscreen,
                    videoLoading: context.state.videoStalled,
                    callPeer: context.state.chatPeer,
                    activatePictureInPicture: activatePictureInPicture,
                    deactivatePictureInPicture: deactivatePictureInPicture,
                    bringBackControllerForPictureInPictureDeactivation: { [weak call] completed in
                        guard let call = call else {
                            completed()
                            return
                        }
                        
                        call.accountContext.sharedContext.mainWindow?.inCallNavigate?()
                        completed()
                    },
                    pictureInPictureClosed: { [weak call] in
                        let _ = call?.leave(terminateIfPossible: false)
                    },
                    onVideoSizeRetrieved: { [weak state] size in
                        state?.videoSize = size
                    },
                    onVideoPlaybackLiveChange: { [weak state] isLive in
                        guard let state else { return }
                        let wasLive = !state.videoStalled
                        if isLive != wasLive {
                            state.videoStalled = !isLive
                            state.updated()
                        }
                    }
                ),
                availableSize: context.availableSize,
                transition: context.transition
            )
            
            var navigationRightItems: [AnyComponentWithIdentity<Empty>] = []
            
            //            let videoIsPlayable = context.state.videoIsPlayable
            //            if state.wantsPiP && state.hasVideo {
            //                state.wantsPiP = false
            //                DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
            //                    activatePictureInPicture.invoke(Action {
            //                        guard let controller = controller() as? MediaStreamComponentController else {
            //                            return
            //                        }
            //                        controller.dismiss(closing: false, manual: true)
            //                    })
            //                }
            //            }
            
            if context.state.isPictureInPictureSupported {
                navigationRightItems.append(AnyComponentWithIdentity(id: "pip", component: AnyComponent(Button(
                    content: AnyComponent(ZStack([
                        AnyComponentWithIdentity(id: "b", component: AnyComponent(Circle(
                            fillColor: .white.withAlphaComponent(0.08),
                            size: CGSize(width: 32.0, height: 32.0)
                        ))),
                        AnyComponentWithIdentity(id: "a", component: AnyComponent(BundleIconComponent(
                            name: "Call/pip",
                            tintColor: .white // .withAlphaComponent(context.state.videoIsPlayable ? 1.0 : 0.6)
                        )))
                    ]
                                                )),
                    action: { [weak state] in
                        guard let state, state.hasVideo else {
                            guard let controller = controller() as? MediaStreamComponentController else {
                                return
                            }
                            //                            state?.wantsPiP = true
                            controller.dismiss(closing: false, manual: true)
                            return
                        }
                        
                        activatePictureInPicture.invoke(Action {
                            guard let controller = controller() as? MediaStreamComponentController else {
                                return
                            }
                            controller.dismiss(closing: false, manual: true)
                            if state.displayUI {
                                state.toggleDisplayUI()
                            }
                        })
                    }
                ).minSize(CGSize(width: 44.0, height: 44.0)))))
            }
            var topLeftButton: AnyComponent<Empty>?
            
            if context.state.canManageCall {
                let whiteColor = UIColor(white: 1.0, alpha: 1.0)
                topLeftButton = AnyComponent(Button(
                    content: AnyComponent(ZStack([
                        AnyComponentWithIdentity(id: "b", component: AnyComponent(Circle(
                            fillColor: .white.withAlphaComponent(0.08),
                            size: CGSize(width: 32.0, height: 32.0)
                        ))),
                        AnyComponentWithIdentity(id: "a", component: AnyComponent(LottieAnimationComponent(
                            animation: LottieAnimationComponent.AnimationItem(
                                name: "anim_profilemore",
                                mode: .still(position: .begin)
                            ),
                            colors: [
                                "Point 2.Group 1.Fill 1": whiteColor,
                                "Point 3.Group 1.Fill 1": whiteColor,
                                "Point 1.Group 1.Fill 1": whiteColor
                            ],
                            size: CGSize(width: 32.0, height: 32.0)
                        ).tagged(moreAnimationTag))),
                    ])),
                    action: { [weak call, weak state] in
                        guard let call = call, let state = state else {
                            return
                        }
                        guard let controller = controller() as? MediaStreamComponentController else {
                            return
                        }
                        guard let anchorView = controller.node.hostView.findTaggedView(tag: moreButtonTag) else {
                            return
                        }
                        
                        if let animationView = controller.node.hostView.findTaggedView(tag: moreAnimationTag) as? LottieAnimationComponent.View {
                            animationView.playOnce()
                        }
                        
                        let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 }
                        
                        var items: [ContextMenuItem] = []
                        
                        items.append(.action(ContextMenuActionItem(id: nil, text: presentationData.strings.LiveStream_EditTitle, textColor: .primary, textLayout: .singleLine, textFont: .regular, badge: nil, icon: { theme in
                            return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Pencil"), color: theme.actionSheet.primaryTextColor)
                        }, action: { [weak call, weak controller, weak state] _, dismissWithResult in
                            guard let call = call, let controller = controller, let state = state, let chatPeer = state.chatPeer else {
                                return
                            }
                            
                            let initialTitle = state.callTitle ?? ""
                            
                            let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 }
                            
                            let title: String = presentationData.strings.LiveStream_EditTitle
                            let text: String = presentationData.strings.LiveStream_EditTitleText
                            
                            let editController = voiceChatTitleEditController(sharedContext: call.accountContext.sharedContext, account: call.accountContext.account, forceTheme: defaultDarkPresentationTheme, title: title, text: text, placeholder: EnginePeer(chatPeer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), value: initialTitle, maxLength: 40, apply: { [weak call] title in
                                guard let call = call else {
                                    return
                                }
                                
                                let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 }
                                
                                if let title = title, title != initialTitle {
                                    call.updateTitle(title)
                                    
                                    let text: String = title.isEmpty ? presentationData.strings.LiveStream_EditTitleRemoveSuccess : presentationData.strings.LiveStream_EditTitleSuccess(title).string
                                    
                                    let _ = text
                                    //strongSelf.presentUndoOverlay(content: .voiceChatFlag(text: text), action: { _ in return false })
                                }
                            })
                            controller.present(editController, in: .window(.root))
                            
                            dismissWithResult(.default)
                        })))
                        
                        if let recordingStartTimestamp = state.recordingStartTimestamp {
                            items.append(.custom(VoiceChatRecordingContextItem(timestamp: recordingStartTimestamp, action: { [weak call, weak controller] _, dismissWithResult in

                                guard let call = call, let controller = controller else {
                                    return
                                }
                                
                                let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 }

                                let alertController = textAlertController(context: call.accountContext, forceTheme: defaultDarkPresentationTheme, title: nil, text: presentationData.strings.VoiceChat_StopRecordingTitle, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.VoiceChat_StopRecordingStop, action: { [weak call, weak controller] in
                                    guard let call = call, let controller = controller else {
                                        return
                                    }
                                    call.setShouldBeRecording(false, title: nil, videoOrientation: nil)
                                    
                                    let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 }
                                    let text = presentationData.strings.LiveStream_RecordingSaved
                                    
                                    let _ = text
                                    let _ = controller
                                    
                                    /*strongSelf.presentUndoOverlay(content: .forward(savedMessages: true, text: text), action: { [weak self] value in
                                        if case .info = value, let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController {
                                            let context = strongSelf.context
                                            strongSelf.controller?.dismiss(completion: {
                                                Queue.mainQueue().justDispatch {
                                                    context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(context.account.peerId), keepStack: .always, purposefulAction: {}, peekData: nil))
                                                }
                                            })
                                            
                                            return true
                                        }
                                        return false
                                    })*/
                                })])
                                controller.present(alertController, in: .window(.root))
                                
                                dismissWithResult(.dismissWithoutContent)
                            }), false))
                        } else {
                            let text = presentationData.strings.LiveStream_StartRecording
                            items.append(.action(ContextMenuActionItem(text: text, icon: { theme -> UIImage? in
                                return generateStartRecordingIcon(color: theme.actionSheet.primaryTextColor)
                            }, action: { [weak call, weak state, weak controller] _, f in
                                f(.dismissWithoutContent)

                                guard let call = call, let state = state, let _ = state.chatPeer, let controller = controller else {
                                    return
                                }
                                
                                let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 }
                                
                                let title: String
                                let text: String
                                let placeholder: String = presentationData.strings.VoiceChat_RecordingTitlePlaceholderVideo
                                
                                title = presentationData.strings.LiveStream_StartRecordingTitle
                                text = presentationData.strings.LiveStream_StartRecordingTextVideo
                                
                                let editController = voiceChatTitleEditController(sharedContext: call.accountContext.sharedContext, account: call.accountContext.account, forceTheme: defaultDarkPresentationTheme, title: title, text: text, placeholder: placeholder, value: nil, maxLength: 40, apply: { [weak call, weak controller] title in
                                    guard let call = call, let controller = controller else {
                                        return
                                    }
                                    
                                    let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 }
                                    
                                    if let title = title {
                                        call.setShouldBeRecording(true, title: title, videoOrientation: false)

                                        let text = presentationData.strings.LiveStream_RecordingStarted
                                        let _ = text

                                        let _ = controller
                                        
                                        call.playTone(.recordingStarted)
                                    }
                                })
                                controller.present(editController, in: .window(.root))
                            })))
                        }
                        
                        let credentialsPromise = Promise<GroupCallStreamCredentials>()
                        credentialsPromise.set(call.accountContext.engine.calls.getGroupCallStreamCredentials(peerId: call.peerId, revokePreviousCredentials: false) |> `catch` { _ -> Signal<GroupCallStreamCredentials, NoError> in return .never() })
                        
                        items.append(.action(ContextMenuActionItem(id: nil, text: presentationData.strings.LiveStream_ViewCredentials, textColor: .primary, textLayout: .singleLine, textFont: .regular, badge: nil, icon: { theme in
                            return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.contextMenu.primaryColor, backgroundColor: nil)
                        }, action: { [weak call, weak controller] _, a in
                            guard let call = call, let controller = controller else {
                                return
                            }
                            
                            controller.push(CreateExternalMediaStreamScreen(context: call.accountContext, peerId: call.peerId, credentialsPromise: credentialsPromise, mode: .view))
                            
                            a(.default)
                        })))
                        
                        items.append(.action(ContextMenuActionItem(id: nil, text: presentationData.strings.LiveStream_StopLiveStream, textColor: .destructive, textLayout: .singleLine, textFont: .regular, badge: nil, icon: { theme in
                            return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.contextMenu.destructiveColor, backgroundColor: nil)
                        }, action: { [weak call] _, a in
                            guard let call = call else {
                                return
                            }
                            let alertController = textAlertController(
                                context: call.accountContext,
                                forceTheme: defaultDarkPresentationTheme,
                                title: presentationData.strings.LiveStream_EndConfirmationTitle,
                                text: presentationData.strings.LiveStream_EndConfirmationText,
                                actions: [
                                    TextAlertAction(
                                        type: .genericAction,
                                        title: presentationData.strings.Common_Cancel,
                                        action: {}
                                    ),
                                    TextAlertAction(
                                        type: .destructiveAction,
                                        title: presentationData.strings.VoiceChat_EndConfirmationEnd,
                                        action: { [weak call] in
                                        guard let call = call else {
                                            return
                                        }
                                        let _ = call.leave(terminateIfPossible: true).start()
                                    })
                                ])
                            controller.present(alertController, in: .window(.root))
                            
                            a(.default)
                        })))
                        
                        final class ReferenceContentSource: ContextReferenceContentSource {
                            private let sourceView: UIView
                            
                            init(sourceView: UIView) {
                                self.sourceView = sourceView
                            }
                            
                            func transitionInfo() -> ContextControllerReferenceViewInfo? {
                                return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds)
                            }
                        }
                        
                        let contextController = ContextController(presentationData: presentationData.withUpdated(theme: defaultDarkPresentationTheme), source: .reference(ReferenceContentSource(sourceView: anchorView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil)
                        /*contextController.passthroughTouchEvent = { sourceView, point in
                            guard let strongSelf = self else {
                                return .ignore
                            }

                            let localPoint = strongSelf.view.convert(sourceView.convert(point, to: nil), from: nil)
                            guard let localResult = strongSelf.hitTest(localPoint, with: nil) else {
                                return .dismiss(consume: true, result: nil)
                            }

                            var testView: UIView? = localResult
                            while true {
                                if let testViewValue = testView {
                                    if let node = testViewValue.asyncdisplaykit_node as? PeerInfoHeaderNavigationButton {
                                        node.isUserInteractionEnabled = false
                                        DispatchQueue.main.async {
                                            node.isUserInteractionEnabled = true
                                        }
                                        return .dismiss(consume: false, result: nil)
                                    } else if let node = testViewValue.asyncdisplaykit_node as? PeerInfoVisualMediaPaneNode {
                                        node.brieflyDisableTouchActions()
                                        return .dismiss(consume: false, result: nil)
                                    } else {
                                        testView = testViewValue.superview
                                    }
                                } else {
                                    break
                                }
                            }

                            return .dismiss(consume: true, result: nil)
                        }*/
                        controller.presentInGlobalOverlay(contextController)
                    }
                ).minSize(CGSize(width: 44.0, height: 44.0)).tagged(moreButtonTag))
            }
            
            let navigationComponent = NavigationBarComponent(
                topInset: environment.statusBarHeight,
                sideInset: environment.safeInsets.left,
                backgroundVisible: isFullscreen,
                leftItem: topLeftButton,
                rightItems: navigationRightItems,
                centerItem: AnyComponent(StreamTitleComponent(text: state.callTitle ?? state.peerTitle, isRecording: state.recordingStartTimestamp != nil, isLive: context.state.videoIsPlayable))
            )
            
            if context.state.storedIsFullscreen != isFullscreen {
                context.state.storedIsFullscreen = isFullscreen
                if isFullscreen {
                    context.state.scheduleDismissUI()
                } else {
                    context.state.cancelScheduledDismissUI()
                }
            }
            
            var infoItem: AnyComponent<Empty>?
            if let originInfo = context.state.originInfo {
                infoItem = AnyComponent(OriginInfoComponent(
                    strings: environment.strings,
                    memberCount: originInfo.memberCount
                ))
            }
            let availableSize = context.availableSize
            let safeAreaTop = safeAreaTopInView
            
            let onPanGesture: ((Gesture.PanGestureState) -> Void) = { [weak state] panState in
                guard let state = state else {
                    return
                }
                switch panState {
                case .began:
                    state.initialOffset = state.dismissOffset
                case let .updated(offset):
                    state.updateDismissOffset(value: state.initialOffset + offset.y, interactive: true)
                case let .ended(velocity):
                    if velocity.y > 200.0 {
                        if state.isFullscreen {
                            state.isFullscreen = false
                            state.prevFullscreenOrientation = UIDevice.current.orientation
                            state.dismissOffset = 0.0
                            if canEnforceOrientation, let controller = controller() as? MediaStreamComponentController {
                                controller.updateOrientation(orientation: .portrait)
                            } else {
                                state.updated(transition: .easeInOut(duration: 0.25))
                            }
                        } else {
                            if isFullyDragged || state.initialOffset != 0 {
                                state.updateDismissOffset(value: 0.0, interactive: false)
                            } else {
                                if state.isPictureInPictureSupported {
                                    guard let controller = controller() as? MediaStreamComponentController else {
                                        return
                                    }
                                    if state.hasVideo {
                                        activatePictureInPicture.invoke(Action {
                                            controller.dismiss(closing: false, manual: true)
                                            if state.displayUI {
                                                state.toggleDisplayUI()
                                            }
                                        })
                                    } else {
                                        //                                        state.wantsPiP = true
                                        controller.dismiss(closing: false, manual: true)
                                    }
                                } else {
                                    guard let controller = controller() as? MediaStreamComponentController else {
                                        return
                                    }
                                    controller.dismiss(closing: false, manual: true)
                                }
                            }
                        }
                    } else {
                        if isFullyDragged {
                            state.updateDismissOffset(value: requiredSheetHeight - availableSize.height + safeAreaTop, interactive: false)
                        } else {
                            if velocity.y < -200 {
                                // Expand
                                state.updateDismissOffset(value: requiredSheetHeight - availableSize.height + safeAreaTop, interactive: false)
                            } else {
                                state.updateDismissOffset(value: 0.0, interactive: false)
                            }
                        }
                    }
                }
            }
            
            context.add(background
                .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
                .gesture(.tap { [weak state] in
                    guard let state = state, state.isFullscreen else {
                        return
                    }
                    state.toggleDisplayUI()
                })
                    .gesture(.pan { panState in
                        onPanGesture(panState)
                    })
            )
            
            context.add(dismissTapComponent
                .position(CGPoint(x: context.availableSize.width / 2, y: dismissTapAreaHeight / 2))
                .gesture(.tap {
                    guard let controller = controller() as? MediaStreamComponentController else {
                        return
                    }
                    controller.dismiss(closing: false, manual: true)
                })
                    .gesture(.pan(onPanGesture))
            )
            
            let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 }
            
            let imageRenderScale = UIScreen.main.scale
            let bottomComponent = AnyComponent(ButtonsRowComponent(
                bottomInset: environment.safeInsets.bottom,
                sideInset: environment.safeInsets.left,
                leftItem: AnyComponent(Button(
                    content: AnyComponent(RoundGradientButtonComponent(
                        gradientColors: [UIColor(red: 0.165, green: 0.173, blue: 0.357, alpha: 1).cgColor],
                        image: generateTintedImage(image: UIImage(bundleImageName: "Call/CallShareButton"), color: .white),
                        // TODO: localize:
                        title: presentationData.strings.VoiceChat_ShareShort)),
                    action: {
                        guard let controller = controller() as? MediaStreamComponentController else {
                            return
                        }
                        controller.presentShare()
                    }
                ).minSize(CGSize(width: 65, height: 80))),
                rightItem: AnyComponent(Button(
                    content: AnyComponent(RoundGradientButtonComponent(
                        gradientColors: [
                            UIColor(red: 0.314, green: 0.161, blue: 0.197, alpha: 1).cgColor
                        ],
                        image: generateImage(CGSize(width: 44.0 * imageRenderScale, height: 44 * imageRenderScale), opaque: false, rotatedContext: { size, context in
                            context.clear(CGRect(origin: CGPoint(), size: size))
                            
                            context.translateBy(x: size.width / 2, y: size.height / 2)
                            context.scaleBy(x: 0.4, y: 0.4)
                            context.translateBy(x: -size.width / 2, y: -size.height / 2)
                            let imageColor = UIColor.white
                            let bounds = CGRect(origin: CGPoint(), size: size)
                            context.clear(bounds)
                            let lineWidth: CGFloat = size.width / 7
                            context.setLineWidth(lineWidth - UIScreenPixel)
                            context.setLineCap(.round)
                            context.setStrokeColor(imageColor.cgColor)
                            
                            context.move(to: CGPoint(x: lineWidth / 2 + UIScreenPixel, y: lineWidth / 2 + UIScreenPixel))
                            context.addLine(to: CGPoint(x: size.width - lineWidth / 2 - UIScreenPixel, y: size.height - lineWidth / 2 - UIScreenPixel))
                            context.strokePath()
                            
                            context.move(to: CGPoint(x: size.width - lineWidth / 2 - UIScreenPixel, y: lineWidth / 2 + UIScreenPixel))
                            context.addLine(to: CGPoint(x: lineWidth / 2 + UIScreenPixel, y: size.height - lineWidth / 2 - UIScreenPixel))
                            context.strokePath()
                        }),
                        title: presentationData.strings.VoiceChat_Leave
                    )),
                    action: { [weak call] in
                        let _ = call?.leave(terminateIfPossible: false)
                    }
                ).minSize(CGSize(width: 44.0, height: 44.0))),
                centerItem: AnyComponent(Button(
                    content: AnyComponent(RoundGradientButtonComponent(
                        gradientColors: [
                            UIColor(red: 0.165, green: 0.173, blue: 0.357, alpha: 1).cgColor
                        ],
                        image: generateImage(CGSize(width: 44 * imageRenderScale, height: 44.0 * imageRenderScale), opaque: false, rotatedContext: { size, context in
                            
                            let imageColor = UIColor.white
                            let bounds = CGRect(origin: CGPoint(), size: size)
                            context.clear(bounds)
                            
                            context.setLineWidth(2.4 * imageRenderScale - UIScreenPixel)
                            context.setLineCap(.round)
                            context.setStrokeColor(imageColor.cgColor)
                            
                            let lineSide = size.width / 5
                            let centerOffset = size.width / 20
                            context.move(to: CGPoint(x: size.width / 2 + lineSide, y: size.height / 2 - centerOffset / 2))
                            context.addLine(to: CGPoint(x: size.width / 2 + lineSide, y: size.height / 2 - lineSide))
                            context.addLine(to: CGPoint(x: size.width / 2 + centerOffset / 2, y: size.height / 2 - lineSide))
                            context.move(to: CGPoint(x: size.width / 2 + lineSide, y: size.height / 2 - lineSide))
                            context.addLine(to: CGPoint(x: size.width / 2 + centerOffset, y: size.height / 2 - centerOffset))
                            context.strokePath()
                            
                            context.move(to: CGPoint(x: size.width / 2 - lineSide, y: size.height / 2 + centerOffset / 2))
                            context.addLine(to: CGPoint(x: size.width / 2 - lineSide, y: size.height / 2 + lineSide))
                            context.addLine(to: CGPoint(x: size.width / 2 - centerOffset / 2, y: size.height / 2 + lineSide))
                            context.move(to: CGPoint(x: size.width / 2 - lineSide, y: size.height / 2 + lineSide))
                            context.addLine(to: CGPoint(x: size.width / 2 - centerOffset, y: size.height / 2 + centerOffset))
                            context.strokePath()
                        }),
                        title: presentationData.strings.LiveStream_Expand
                    )),
                    action: { [weak state] in
                        guard let state = state else { return }
                        
                        if let controller = controller() as? MediaStreamComponentController {
                            state.isFullscreen.toggle()
                            if state.isFullscreen {
                                state.dismissOffset = 0.0
                                let currentOrientation = state.prevFullscreenOrientation ?? UIDevice.current.orientation
                                switch currentOrientation {
                                case .landscapeLeft:
                                    controller.updateOrientation(orientation: .landscapeRight)
                                case .landscapeRight:
                                    controller.updateOrientation(orientation: .landscapeLeft)
                                default:
                                    controller.updateOrientation(orientation: .landscapeRight)
                                }
                            } else {
                                state.prevFullscreenOrientation = UIDevice.current.orientation
                                controller.updateOrientation(orientation: .portrait)
                            }
                            if !canEnforceOrientation {
                                state.updated(transition: .easeInOut(duration: 0.25))
                            }
                        }
                    }
                ).minSize(CGSize(width: 44.0, height: 44.0)))
            ))
            
            let sheetHeight: CGFloat = max(requiredSheetHeight - dragOffset, requiredSheetHeight)
            let topOffset: CGFloat = isFullscreen
            ? max(context.state.dismissOffset, 0)
            : (context.availableSize.height - requiredSheetHeight + dragOffset)
            
            let sheet = sheet.update(
                component: StreamSheetComponent(
                    strings: environment.strings,
                    topOffset: topOffset,
                    sheetHeight: sheetHeight,
                    backgroundColor: (isFullscreen && !state.hasVideo) ? .clear : (isFullyDragged ? fullscreenBackgroundColor : panelBackgroundColor),
                    bottomPadding: bottomPadding,
                    participantsCount: context.state.originInfo?.memberCount ?? 0, // Int.random(in: 0...999998) // [0, 5, 15, 16, 95, 100, 16042, 942539].randomElement()!
                    isFullyExtended: isFullyDragged,
                    deviceCornerRadius: ((controller() as? MediaStreamComponentController)?.validLayout?.deviceMetrics.screenCornerRadius ?? 1) - 1,
                    videoHeight: videoHeight,
                    isFullscreen: isFullscreen,
                    fullscreenTopComponent: AnyComponent(navigationComponent),
                    fullscreenBottomComponent: bottomComponent
                ),
                availableSize: context.availableSize,
                transition: context.transition
            )
            
            context.add(sheet
                .position(.init(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2))
            )
            
            var availableWidth: CGFloat { context.availableSize.width }
            var contentHeight: CGFloat { 44.0 }
            
            let topItem = topItem.update(
                component: AnyComponent(navigationComponent),
                availableSize: CGSize(width: availableWidth, height: contentHeight),
                transition: context.transition
            )
            
            let fullScreenToolbarComponent = AnyComponent(ToolbarComponent(
                bottomInset: environment.safeInsets.bottom,
                sideInset: environment.safeInsets.left,
                leftItem: AnyComponent(Button(
                    content: AnyComponent(BundleIconComponent(
                        name: "Chat/Input/Accessory Panels/MessageSelectionForward",
                        tintColor: .white
                    )),
                    action: {
                        guard let controller = controller() as? MediaStreamComponentController else {
                            return
                        }
                        controller.presentShare()
                    }
                ).minSize(CGSize(width: 64.0, height: 80))),
                rightItem: /*state.hasVideo ?*/ AnyComponent(Button(
                    content: AnyComponent(BundleIconComponent(
                        name: isFullscreen ? "Media Gallery/Minimize" : "Media Gallery/Fullscreen",
                        tintColor: .white
                    )),
                    action: {
                        state.isFullscreen = false
                        state.prevFullscreenOrientation = UIDevice.current.orientation
                        if let controller = controller() as? MediaStreamComponentController {
                            if canEnforceOrientation {
                                controller.updateOrientation(orientation: .portrait)
                            } else {
                                state.updated(transition: .easeInOut(duration: 0.25))
                            }
                        }
                    }
                ).minSize(CGSize(width: 64.0, height: 80.0))),
                centerItem: infoItem
            ))
            
            let buttonsRow = buttonsRow.update(
                component: bottomComponent,
                availableSize: CGSize(width: availableWidth, height: contentHeight),
                transition: context.transition
            )
            
            let fullscreenBottomItem = fullscreenBottomItem.update(
                component: fullScreenToolbarComponent,
                availableSize: CGSize(width: availableWidth, height: contentHeight),
                transition: context.transition
            )
            
            let videoPos: CGFloat
            
            if isFullscreen {
                videoPos = context.availableSize.height / 2 + dragOffset
            } else {
                videoPos = topOffset + 28.0 + 28.0 + videoHeight / 2
            }
            context.add(video
                .position(CGPoint(x: context.availableSize.width / 2.0, y: videoPos))
            )
            
            context.add(topItem
                .position(CGPoint(x: topItem.size.width / 2.0, y: topOffset + (isFullscreen ? topItem.size.height / 2.0 : 28.0)))
                .opacity((!isFullscreen || state.displayUI) ? 1.0 : 0.0)
                .gesture(.pan { panState in
                    onPanGesture(panState)
                })
            )
            
            context.add(buttonsRow
                .opacity(isFullscreen ? 0.0 : 1.0)
                .position(CGPoint(x: buttonsRow.size.width / 2, y: sheetHeight - 50.0 / 2 + topOffset - bottomPadding))
            )
            
            context.add(fullscreenBottomItem
                .opacity((isFullscreen && state.displayUI) ? 1.0 : 0.0)
                .position(CGPoint(x: fullscreenBottomItem.size.width / 2, y: context.availableSize.height - fullscreenBottomItem.size.height / 2 + topOffset - 0.0))
            )
            return context.availableSize
        }
        return makeBody()
    }
    
}

public final class MediaStreamComponentController: ViewControllerComponentContainer, VoiceChatController {
    private let context: AccountContext
    public let call: PresentationGroupCall
    public private(set) var currentOverlayController: VoiceChatOverlayController? = nil
    public var parentNavigationController: NavigationController?
    
    public var onViewDidAppear: (() -> Void)?
    public var onViewDidDisappear: (() -> Void)?
    
    private var initialOrientation: UIInterfaceOrientation?
    
    private let inviteLinksPromise = Promise<GroupCallInviteLinks?>(nil)
    
    public init(call: PresentationGroupCall) {
        self.context = call.accountContext
        self.call = call
        
        super.init(context: call.accountContext, component: MediaStreamComponent(call: call as! PresentationGroupCallImpl), navigationBarAppearance: .none)
        
        self.statusBar.statusBarStyle = .White
        self.view.disablesInteractiveModalDismiss = true
        
        self.inviteLinksPromise.set(.single(nil)
        |> then(call.inviteLinks))
    }
    
    required public init(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override public func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        DispatchQueue.main.async {
            self.onViewDidAppear?()
        }
        
        if let view = self.node.hostView.findTaggedView(tag: MediaStreamVideoComponent.View.Tag()) as? MediaStreamVideoComponent.View {
            view.expandFromPictureInPicture()
        }
        
            self.view.clipsToBounds = true
            
            self.view.layer.animatePosition(from: CGPoint(x: self.view.frame.center.x, y: self.view.bounds.maxY + self.view.bounds.height / 2), to: self.view.center, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
            })
        
        self.view.layer.allowsGroupOpacity = true
        
        self.backgroundDimView.layer.animateAlpha(from: 0, to: 1, duration: 0.3, completion: { [weak self] _ in
            guard let strongSelf = self else {
                return
            }
            strongSelf.view.layer.allowsGroupOpacity = false
        })
        if backgroundDimView.superview == nil {
            guard let superview = view.superview else { return }
            superview.insertSubview(backgroundDimView, belowSubview: view)
        }
    }
    
    override public func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        
        DispatchQueue.main.async {
            self.onViewDidDisappear?()
        }
    }
    
    override public func viewDidLoad() {
        super.viewDidLoad()
        // TODO: replace with actual color
        backgroundDimView.backgroundColor = .black.withAlphaComponent(0.3)
        self.view.clipsToBounds = false
    }
    
    
    override public func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
    }
    
    override public func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        let dimViewSide: CGFloat = max(view.bounds.width, view.bounds.height)
        backgroundDimView.frame = .init(x: view.bounds.midX - dimViewSide / 2, y: -view.bounds.height * 3, width: dimViewSide, height: view.bounds.height * 4)
    }
    
    public func dismiss(closing: Bool, manual: Bool) {
        self.dismiss(completion: nil)
    }
    
    let backgroundDimView = UIView()
    
    override public func dismiss(completion: (() -> Void)? = nil) {
        self.view.layer.allowsGroupOpacity = true
        
        self.backgroundDimView.layer.animateAlpha(from: 1.0, to: 0, duration: 0.3, removeOnCompletion: false)
        self.view.layer.animatePosition(from: self.view.center, to: CGPoint(x: self.view.center.x, y: self.view.bounds.maxY + self.view.bounds.height / 2), duration: 0.4, removeOnCompletion: false, completion: { [weak self] _ in
            guard let strongSelf = self else {
                completion?()
                return
            }
            strongSelf.view.layer.allowsGroupOpacity = false
            strongSelf.dismissImpl(completion: completion)
        })
    }
    
    private func dismissImpl(completion: (() -> Void)? = nil) {
        super.dismiss(completion: completion)
    }
    
    func updateOrientation(orientation: UIInterfaceOrientation) {
        if self.initialOrientation == nil {
            self.initialOrientation = orientation == .portrait ? .landscapeRight : .portrait
        } else if self.initialOrientation == orientation {
            self.initialOrientation = nil
        }
        self.call.accountContext.sharedContext.applicationBindings.forceOrientation(orientation)
    }
    
    func presentShare() {
        let _ = (self.inviteLinksPromise.get()
        |> take(1)
        |> deliverOnMainQueue).start(next: { [weak self] inviteLinks in
            guard let strongSelf = self else {
                return
            }
            
            let _ = (strongSelf.context.engine.data.get(
                TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.call.peerId),
                TelegramEngine.EngineData.Item.Peer.ExportedInvitation(id: strongSelf.call.peerId)
            )
            |> map { peer, exportedInvitation -> GroupCallInviteLinks? in
                if let inviteLinks = inviteLinks {
                    return inviteLinks
                } else if let peer = peer, let addressName = peer.addressName, !addressName.isEmpty {
                    return GroupCallInviteLinks(listenerLink: "https://t.me/\(addressName)?voicechat", speakerLink: nil)
                } else if let link = exportedInvitation?.link {
                    return GroupCallInviteLinks(listenerLink: link, speakerLink: nil)
                }
                return nil
            }
            |> deliverOnMainQueue).start(next: { links in
                guard let strongSelf = self else {
                    return
                }
                
                if let links = links {
                    strongSelf.presentShare(links: links)
                }
            })
        })
    }
        
    func presentShare(links inviteLinks: GroupCallInviteLinks) {
        let formatSendTitle: (String) -> String = { string in
            var string = string
            if string.contains("[") && string.contains("]") {
                if let startIndex = string.firstIndex(of: "["), let endIndex = string.firstIndex(of: "]") {
                    string.removeSubrange(startIndex ... endIndex)
                }
            } else {
                string = string.trimmingCharacters(in: CharacterSet(charactersIn: "0123456789-,."))
            }
            return string
        }
        let _ = formatSendTitle
        
        let _ = (combineLatest(queue: .mainQueue(), self.context.account.postbox.loadedPeerWithId(self.call.peerId), self.call.state |> take(1))
        |> deliverOnMainQueue).start(next: { [weak self] peer, callState in
            if let strongSelf = self {
                var inviteLinks = inviteLinks
                
                if let peer = peer as? TelegramChannel, case .group = peer.info, !peer.flags.contains(.isGigagroup), !(peer.addressName ?? "").isEmpty, let defaultParticipantMuteState = callState.defaultParticipantMuteState {
                    let isMuted = defaultParticipantMuteState == .muted
                    
                    if !isMuted {
                        inviteLinks = GroupCallInviteLinks(listenerLink: inviteLinks.listenerLink, speakerLink: nil)
                    }
                }
                
                let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
                
                var segmentedValues: [ShareControllerSegmentedValue]?
                segmentedValues = nil
                let shareController = ShareController(context: strongSelf.context, subject: .url(inviteLinks.listenerLink), segmentedValues: segmentedValues, forceTheme: defaultDarkPresentationTheme, forcedActionTitle: presentationData.strings.VoiceChat_CopyInviteLink)
                shareController.completed = { [weak self] peerIds in
                    if let strongSelf = self {
                        let _ = (strongSelf.context.engine.data.get(
                            EngineDataList(
                                peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init)
                            )
                        )
                        |> deliverOnMainQueue).start(next: { [weak self] peerList in
                            if let strongSelf = self {
                                let peers = peerList.compactMap { $0 }
                                let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
                                
                                let text: String
                                var isSavedMessages = false
                                if peers.count == 1, let peer = peers.first {
                                    isSavedMessages = peer.id == strongSelf.context.account.peerId
                                    let peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
                                    text = presentationData.strings.VoiceChat_ForwardTooltip_Chat(peerName).string
                                } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last {
                                    let firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
                                    let secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
                                    text = presentationData.strings.VoiceChat_ForwardTooltip_TwoChats(firstPeerName, secondPeerName).string
                                } else if let peer = peers.first {
                                    let peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
                                    text = presentationData.strings.VoiceChat_ForwardTooltip_ManyChats(peerName, "\(peers.count - 1)").string
                                } else {
                                    text = ""
                                }
                                
                                strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: isSavedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
                            }
                        })
                    }
                }
                shareController.actionCompleted = {
                    if let strongSelf = self {
                        let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
                        strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.VoiceChat_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
                    }
                }
                strongSelf.present(shareController, in: .window(.root))
            }
        })
    }
}

// MARK: - Subcomponents

private final class NavigationBarComponent: CombinedComponent {
    let topInset: CGFloat
    let sideInset: CGFloat
    let leftItem: AnyComponent<Empty>?
    let rightItems: [AnyComponentWithIdentity<Empty>]
    let centerItem: AnyComponent<Empty>?
    let backgroundVisible: Bool
    
    init(
        topInset: CGFloat,
        sideInset: CGFloat,
        backgroundVisible: Bool,
        leftItem: AnyComponent<Empty>?,
        rightItems: [AnyComponentWithIdentity<Empty>],
        centerItem: AnyComponent<Empty>?
    ) {
        self.topInset = 0 // topInset
        self.sideInset = sideInset
        self.backgroundVisible = backgroundVisible
        
        self.leftItem = leftItem
        self.rightItems = rightItems
        self.centerItem = centerItem
    }
    
    static func ==(lhs: NavigationBarComponent, rhs: NavigationBarComponent) -> Bool {
        if lhs.topInset != rhs.topInset {
            return false
        }
        if lhs.sideInset != rhs.sideInset {
            return false
        }
        if lhs.leftItem != rhs.leftItem {
            return false
        }
        if lhs.rightItems != rhs.rightItems {
            return false
        }
        if lhs.centerItem != rhs.centerItem {
            return false
        }
        
        return true
    }
    
    static var body: Body {
        let background = Child(Rectangle.self)
        let leftItem = Child(environment: Empty.self)
        let rightItems = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self)
        let centerItem = Child(environment: Empty.self)
        
        return { context in
            var availableWidth = context.availableSize.width
            let sideInset: CGFloat = 16.0 + context.component.sideInset
            
            let contentHeight: CGFloat = 44.0
            let size = CGSize(width: context.availableSize.width, height: context.component.topInset + contentHeight)
            
            let background = background.update(
                component: Rectangle(color: UIColor(white: 0.0, alpha: 0.5)),
                availableSize: CGSize(width: size.width, height: size.height),
                transition: context.transition
            )
            
            let leftItem = context.component.leftItem.flatMap { leftItemComponent in
                return leftItem.update(
                    component: leftItemComponent,
                    availableSize: CGSize(width: availableWidth, height: contentHeight),
                    transition: context.transition
                )
            }
            if let leftItem = leftItem {
                availableWidth -= leftItem.size.width
            }
            
            var rightItemList: [_UpdatedChildComponent] = []
            for item in context.component.rightItems {
                let item = rightItems[item.id].update(
                    component: item.component,
                    availableSize: CGSize(width: availableWidth, height: contentHeight),
                    transition: context.transition
                )
                rightItemList.append(item)
                availableWidth -= item.size.width
            }
            
            let centerItem = context.component.centerItem.flatMap { centerItemComponent in
                return centerItem.update(
                    component: centerItemComponent,
                    availableSize: CGSize(width: availableWidth - 44.0 - 44.0, height: contentHeight),
                    transition: context.transition
                )
            }
            if let centerItem = centerItem {
                availableWidth -= centerItem.size.width
            }
            
            context.add(background
                .position(CGPoint(x: size.width / 2.0, y: size.height / 2.0))
                .opacity(context.component.backgroundVisible ? 1 : 0)
            )
            
            var centerLeftInset = sideInset
            if let leftItem = leftItem {
                context.add(leftItem
                    .position(CGPoint(x: sideInset + leftItem.size.width / 2.0, y: context.component.topInset + contentHeight / 2.0))
                )
                centerLeftInset += leftItem.size.width + 4.0
            }
            
            var rightItemX = context.availableSize.width - sideInset
            for item in rightItemList.reversed() {
                context.add(item
                    .position(CGPoint(x: rightItemX - item.size.width / 2.0, y: context.component.topInset + contentHeight / 2.0))
                )
                rightItemX -= item.size.width + 8.0
            }
            
            let accumulatedOffset: CGFloat = 16.0
            if let centerItem = centerItem {
                context.add(centerItem
                    .position(CGPoint(x: context.availableSize.width / 2 - accumulatedOffset, y: context.component.topInset + contentHeight / 2.0))
                )
            }
            
            return size
        }
    }
}

private final class StreamTitleComponent: Component {
    private final class LiveIndicatorView: UIView {
        private let label = UILabel()
        private let stalledAnimatedGradient = CAGradientLayer()
        private var wasLive = false
        
        var desiredWidth: CGFloat { label.intrinsicContentSize.width + 6.0 + 6.0 }
        
        override init(frame: CGRect = .zero) {
            super.init(frame: frame)
            
            self.addSubview(label)
            
            let liveString = NSAttributedString(
                string: "LIVE",
                attributes: [
                    .font: Font.with(size: 11.0, design: .round, weight: .bold),
                    .paragraphStyle: {
                        let style = NSMutableParagraphStyle()
                        style.alignment = .center
                        return style
                    }(),
                    .foregroundColor: UIColor.white,
                    .kern: -0.6
                ]
            )
            self.label.attributedText = liveString
            
            self.layer.addSublayer(stalledAnimatedGradient)
            self.clipsToBounds = true
            self.toggle(isLive: false)
        }
        
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
        
        override func layoutSubviews() {
            super.layoutSubviews()
            
            label.frame = bounds
            stalledAnimatedGradient.frame = bounds
            self.layer.cornerRadius = min(bounds.width, bounds.height) / 2
        }
        
        func toggle(isLive: Bool) {
            if isLive {
                if !self.wasLive {
                    self.wasLive = true
                    let anim = CAKeyframeAnimation(keyPath: "transform.scale")
                    anim.values = [1.0, 1.12, 0.9, 1.0]
                    anim.keyTimes = [0, 0.5, 0.8, 1]
                    anim.duration = 0.4
                    self.layer.add(anim, forKey: "transform")
                    
                    UIView.animate(withDuration: 0.15, animations: {
                        self.toggle(isLive: true) })
                    return
                }
                self.backgroundColor = UIColor(red: 1, green: 0.176, blue: 0.333, alpha: 1)
                self.stalledAnimatedGradient.opacity = 0
                self.stalledAnimatedGradient.removeAllAnimations()
            } else {
                if wasLive {
                    wasLive = false
                    UIView.animate(withDuration: 0.3) {
                        self.toggle(isLive: false)
                    }
                    return
                }
                self.backgroundColor = UIColor(white: 0.36, alpha: 1)
                stalledAnimatedGradient.opacity = 1
            }
            wasLive = isLive
        }
    }
    
    private let text: String
    private let isRecording: Bool
    private let isLive: Bool
    
    init(text: String, isRecording: Bool, isLive: Bool) {
        self.text = text
        self.isRecording = isRecording
        self.isLive = isLive
    }
    
    static func ==(lhs: StreamTitleComponent, rhs: StreamTitleComponent) -> Bool {
        if lhs.text != rhs.text {
            return false
        }
        if lhs.isRecording != rhs.isRecording {
            return false
        }
        if lhs.isLive != rhs.isLive {
            return false
        }
        return false
    }
    
    public final class View: UIView {
        private var indicatorView: UIImageView?
        private let liveIndicatorView = LiveIndicatorView()
        private let titleLabel = UILabel()
        private var titleFadeLayer = CALayer()
        
        private let trackingLayer: HierarchyTrackingLayer
        
        override init(frame: CGRect) {
            self.trackingLayer = HierarchyTrackingLayer()
            
            super.init(frame: frame)
            
            self.addSubview(self.titleLabel)
            self.addSubview(self.liveIndicatorView)
            
            self.trackingLayer.didEnterHierarchy = { [weak self] in
                guard let strongSelf = self else {
                    return
                }
                strongSelf.updateIndicatorAnimation()
            }
        }
        
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
        
        private func updateIndicatorAnimation() {
            guard let indicatorView = self.indicatorView else {
                return
            }
            if indicatorView.layer.animation(forKey: "blink") == nil {
                let animation = CAKeyframeAnimation(keyPath: "opacity")
                animation.values = [1.0 as NSNumber, 1.0 as NSNumber, 0.55 as NSNumber]
                animation.keyTimes = [0.0 as NSNumber, 0.4546 as NSNumber, 0.9091 as NSNumber, 1 as NSNumber]
                animation.duration = 0.7
                animation.autoreverses = true
                animation.repeatCount = Float.infinity
                indicatorView.layer.add(animation, forKey: "recording")
            }
        }
        
        func update(component: StreamTitleComponent, availableSize: CGSize, transition: Transition) -> CGSize {
            let liveIndicatorWidth: CGFloat = self.liveIndicatorView.desiredWidth
            let liveIndicatorHeight: CGFloat = 20.0
            
            let currentText = self.titleLabel.text
            if currentText != component.text {
                if currentText?.isEmpty == false {
                    UIView.transition(with: self.titleLabel, duration: 0.2) {
                        self.titleLabel.text = component.text
                        self.titleLabel.invalidateIntrinsicContentSize()
                    }
                } else {
                    self.titleLabel.text = component.text
                    self.titleLabel.invalidateIntrinsicContentSize()
                }
            }
            self.titleLabel.font = Font.semibold(17.0)
            self.titleLabel.textColor = .white
            self.titleLabel.numberOfLines = 1
            
            let textSize = CGSize(width: min(availableSize.width - 4 - liveIndicatorWidth, self.titleLabel.intrinsicContentSize.width), height: availableSize.height)
            
            if component.isRecording {
                if self.indicatorView == nil {
                    let indicatorView = UIImageView(image: generateFilledCircleImage(diameter: 8.0, color: .red, strokeColor: nil, strokeWidth: nil, backgroundColor: nil))
                    self.addSubview(indicatorView)
                    self.indicatorView = indicatorView
                    
                    self.updateIndicatorAnimation()
                }
            } else {
                if let indicatorView = self.indicatorView {
                    self.indicatorView = nil
                    indicatorView.removeFromSuperview()
                }
            }
            let sideInset: CGFloat = 20.0
            let size = CGSize(width: textSize.width + sideInset * 2.0, height: textSize.height)
            let textFrame = CGRect(origin: CGPoint(x: sideInset, y: floor((size.height - textSize.height) / 2.0)), size: textSize)
            
            if currentText?.isEmpty == false {
                UIView.transition(with: self.titleLabel, duration: 0.2, options: .transitionCrossDissolve) {
                    self.updateTitleFadeLayer(constrainedTextFrame: textFrame)
                }
            } else {
                self.updateTitleFadeLayer(constrainedTextFrame: textFrame)
            }
            
            liveIndicatorView.frame = CGRect(origin: CGPoint(x: textFrame.maxX + 6.0, y: textFrame.midY - liveIndicatorHeight / 2), size: .init(width: liveIndicatorWidth, height: liveIndicatorHeight))
            self.liveIndicatorView.toggle(isLive: component.isLive)
            
            if let indicatorView = self.indicatorView, let image = indicatorView.image {
                indicatorView.frame = CGRect(origin: CGPoint(x: liveIndicatorView.frame.maxX + 6.0, y: floorToScreenPixels((size.height - image.size.height) / 2.0) + 1.0), size: image.size)
            }
            
            return size
        }
        
        private func updateTitleFadeLayer(constrainedTextFrame: CGRect) {
            guard let textBounds = titleLabel.attributedText.flatMap({ $0.boundingRect(with: CGSize(width: .max, height: .max), context: nil) }),
                textBounds.width > constrainedTextFrame.width
            else {
                titleLabel.layer.mask = nil
                titleLabel.frame = constrainedTextFrame
                self.titleLabel.textAlignment = .center
                return
            }
                 
            var isRTL: Bool = false
            if let string = titleLabel.attributedText {
                let coreTextLine = CTLineCreateWithAttributedString(string)
                let glyphRuns = CTLineGetGlyphRuns(coreTextLine) as NSArray
                if glyphRuns.count > 0 {
                    let run = glyphRuns[0] as! CTRun
                    if CTRunGetStatus(run).contains(CTRunStatus.rightToLeft) {
                        isRTL = true
                    }
                }
            }
            
            let gradientInset: CGFloat = 0.0
            let gradientRadius: CGFloat = 50.0
            let extraSpaceToFitTruncation: CGFloat = 100.0
            
            let solidPartLayer = CALayer()
            solidPartLayer.backgroundColor = UIColor.black.cgColor
            
            let availableWidth: CGFloat = constrainedTextFrame.width - gradientRadius
            
            if isRTL {
                solidPartLayer.frame = CGRect(
                    origin: CGPoint(x: constrainedTextFrame.width + extraSpaceToFitTruncation - availableWidth, y: 0),
                    size: CGSize(width: availableWidth, height: constrainedTextFrame.height))
                
                self.titleLabel.textAlignment = .right
                
                titleLabel.frame = CGRect(x: constrainedTextFrame.minX - extraSpaceToFitTruncation, y: constrainedTextFrame.minY, width: constrainedTextFrame.width + extraSpaceToFitTruncation, height: constrainedTextFrame.height)
            } else {
                self.titleLabel.textAlignment = .left
                
                solidPartLayer.frame = CGRect(
                    origin: .zero,
                    size: CGSize(width: availableWidth, height: constrainedTextFrame.height))
                titleLabel.frame = CGRect(origin: constrainedTextFrame.origin, size: CGSize(width: constrainedTextFrame.width + extraSpaceToFitTruncation, height: constrainedTextFrame.height))
            }
            
            titleFadeLayer = CALayer()
            titleFadeLayer.addSublayer(solidPartLayer)
            
            let gradientLayer = CAGradientLayer()
            gradientLayer.colors = [UIColor.red.cgColor, UIColor.clear.cgColor]
            if isRTL {
                gradientLayer.startPoint = CGPoint(x: 1, y: 0.5)
                gradientLayer.endPoint = CGPoint(x: 0, y: 0.5)
                gradientLayer.frame = CGRect(x: solidPartLayer.frame.minX - gradientRadius, y: 0, width: gradientRadius, height: constrainedTextFrame.height)
            } else {
                gradientLayer.startPoint = CGPoint(x: 0, y: 0.5)
                gradientLayer.endPoint = CGPoint(x: 1, y: 0.5)
                gradientLayer.frame = CGRect(x: availableWidth + gradientInset, y: 0, width: gradientRadius, height: constrainedTextFrame.height)
            }
            titleFadeLayer.addSublayer(gradientLayer)
            titleFadeLayer.masksToBounds = false
            
            titleFadeLayer.frame = titleLabel.bounds
            
            titleLabel.layer.mask = titleFadeLayer
        }
        
    }
    
    public func makeView() -> View {
        return View(frame: CGRect())
    }
    
    public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
        return view.update(component: self, availableSize: availableSize, transition: transition)
    }
}


private final class OriginInfoComponent: CombinedComponent {
    let strings: PresentationStrings
    let participantsCount: Int
    
    init(
        strings: PresentationStrings,
        memberCount: Int
    ) {
        self.strings = strings
        self.participantsCount = memberCount
    }
    
    static func ==(lhs: OriginInfoComponent, rhs: OriginInfoComponent) -> Bool {
        if lhs.strings !== rhs.strings {
            return false
        }
        if lhs.participantsCount != rhs.participantsCount {
            return false
        }
        
        return true
    }
    
    static var body: Body {
        let viewerCounter = Child(ParticipantsComponent.self)
        
        return { context in
            let viewerCounter = viewerCounter.update(
                component: ParticipantsComponent(
                    strings: context.component.strings,
                    count: context.component.participantsCount,
                    showsSubtitle: true,
                    fontSize: 18.0,
                    gradientColors: [UIColor.white.cgColor]
                ),
                availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height),
                transition: context.transition
            )
            let heightReduction: CGFloat = 16.0
            var size = CGSize(width: viewerCounter.size.width, height: viewerCounter.size.height - heightReduction)
            size.width = min(size.width, context.availableSize.width)
            size.height = min(size.height, context.availableSize.height)
            
            context.add(viewerCounter
                .position(CGPoint(x: size.width / 2.0, y: context.availableSize.height / 2.0 + 16.0 - heightReduction / 2))
            )
            
            return size
        }
    }
}

private final class ToolbarComponent: CombinedComponent {
    let bottomInset: CGFloat
    let sideInset: CGFloat
    let leftItem: AnyComponent<Empty>?
    let rightItem: AnyComponent<Empty>?
    let centerItem: AnyComponent<Empty>?
    
    init(
        bottomInset: CGFloat,
        sideInset: CGFloat,
        leftItem: AnyComponent<Empty>?,
        rightItem: AnyComponent<Empty>?,
        centerItem: AnyComponent<Empty>?
    ) {
        self.bottomInset = bottomInset
        self.sideInset = sideInset
        self.leftItem = leftItem
        self.rightItem = rightItem
        self.centerItem = centerItem
    }
    
    static func ==(lhs: ToolbarComponent, rhs: ToolbarComponent) -> Bool {
        if lhs.bottomInset != rhs.bottomInset {
            return false
        }
        if lhs.sideInset != rhs.sideInset {
            return false
        }
        if lhs.leftItem != rhs.leftItem {
            return false
        }
        if lhs.rightItem != rhs.rightItem {
            return false
        }
        if lhs.centerItem != rhs.centerItem {
            return false
        }
        
        return true
    }
    
    static var body: Body {
        let background = Child(Rectangle.self)
        let leftItem = Child(environment: Empty.self)
        let rightItem = Child(environment: Empty.self)
        let centerItem = Child(environment: Empty.self)
        
        return { context in
            var availableWidth = context.availableSize.width
            let sideInset: CGFloat = 16.0 + context.component.sideInset
            
            let contentHeight: CGFloat = 44.0
            let size = CGSize(width: context.availableSize.width, height: contentHeight + context.component.bottomInset)
            
            let background = background.update(component: Rectangle(color: UIColor(white: 0.0, alpha: 0.5)), availableSize: CGSize(width: size.width, height: size.height), transition: context.transition)
            
            let leftItem = context.component.leftItem.flatMap { leftItemComponent in
                return leftItem.update(
                    component: leftItemComponent,
                    availableSize: CGSize(width: availableWidth, height: contentHeight),
                    transition: context.transition
                )
            }
            if let leftItem = leftItem {
                availableWidth -= leftItem.size.width
            }
            
            let rightItem = context.component.rightItem.flatMap { rightItemComponent in
                return rightItem.update(
                    component: rightItemComponent,
                    availableSize: CGSize(width: availableWidth, height: contentHeight),
                    transition: context.transition
                )
            }
            if let rightItem = rightItem {
                availableWidth -= rightItem.size.width
            }
            
            let temporaryOffsetForSmallerSubtitle: CGFloat = 12
            let centerItem = context.component.centerItem.flatMap { centerItemComponent in
                return centerItem.update(
                    component: centerItemComponent,
                    availableSize: CGSize(width: availableWidth, height: contentHeight - temporaryOffsetForSmallerSubtitle / 2),
                    transition: context.transition
                )
            }
            if let centerItem = centerItem {
                availableWidth -= centerItem.size.width
            }
            
            context.add(background
                .position(CGPoint(x: size.width / 2.0, y: size.height / 2.0))
            )
            
            var centerLeftInset = sideInset
            if let leftItem = leftItem {
                context.add(leftItem
                    .position(CGPoint(x: sideInset + leftItem.size.width / 2.0, y: contentHeight / 2.0))
                )
                centerLeftInset += leftItem.size.width + 4.0
            }
            
            var centerRightInset = sideInset
            if let rightItem = rightItem {
                context.add(rightItem
                    .position(CGPoint(x: context.availableSize.width - sideInset - rightItem.size.width / 2.0, y: contentHeight / 2.0))
                )
                centerRightInset += rightItem.size.width + 4.0
            }
            
            let maxCenterInset = max(centerLeftInset, centerRightInset)
            if let centerItem = centerItem {
                context.add(centerItem
                    .position(CGPoint(x: maxCenterInset + (context.availableSize.width - maxCenterInset - maxCenterInset) / 2.0, y: contentHeight / 2.0 - temporaryOffsetForSmallerSubtitle))
                )
            }
            
            return size
        }
    }
}

private final class ButtonsRowComponent: CombinedComponent {
    let bottomInset: CGFloat
    let sideInset: CGFloat
    let leftItem: AnyComponent<Empty>?
    let rightItem: AnyComponent<Empty>?
    let centerItem: AnyComponent<Empty>?
    
    init(
        bottomInset: CGFloat,
        sideInset: CGFloat,
        leftItem: AnyComponent<Empty>?,
        rightItem: AnyComponent<Empty>?,
        centerItem: AnyComponent<Empty>?
    ) {
        self.bottomInset = bottomInset
        self.sideInset = sideInset
        self.leftItem = leftItem
        self.rightItem = rightItem
        self.centerItem = centerItem
    }
    
    static func ==(lhs: ButtonsRowComponent, rhs: ButtonsRowComponent) -> Bool {
        if lhs.bottomInset != rhs.bottomInset {
            return false
        }
        if lhs.sideInset != rhs.sideInset {
            return false
        }
        if lhs.leftItem != rhs.leftItem {
            return false
        }
        if lhs.rightItem != rhs.rightItem {
            return false
        }
        if lhs.centerItem != rhs.centerItem {
            return false
        }
        
        return true
    }
    
    static var body: Body {
        let leftItem = Child(environment: Empty.self)
        let rightItem = Child(environment: Empty.self)
        let centerItem = Child(environment: Empty.self)
        
        return { context in
            var availableWidth = context.availableSize.width
            let sideInset: CGFloat = 48.0 + context.component.sideInset
            
            let contentHeight: CGFloat = 80.0
            let size = CGSize(width: context.availableSize.width, height: contentHeight + context.component.bottomInset)
            
            let leftItem = context.component.leftItem.flatMap { leftItemComponent in
                return leftItem.update(
                    component: leftItemComponent,
                    availableSize: CGSize(width: 50.0, height: contentHeight),
                    transition: context.transition
                )
            }
            if let leftItem = leftItem {
                availableWidth -= leftItem.size.width
            }
            
            let rightItem = context.component.rightItem.flatMap { rightItemComponent in
                return rightItem.update(
                    component: rightItemComponent,
                    availableSize: CGSize(width: 50.0, height: contentHeight),
                    transition: context.transition
                )
            }
            if let rightItem = rightItem {
                availableWidth -= rightItem.size.width
            }
            
            let centerItem = context.component.centerItem.flatMap { centerItemComponent in
                return centerItem.update(
                    component: centerItemComponent,
                    availableSize: CGSize(width: 50.0, height: contentHeight),
                    transition: context.transition
                )
            }
            if let centerItem = centerItem {
                availableWidth -= centerItem.size.width
            }
            
            var centerLeftInset = sideInset
            if let leftItem = leftItem {
                context.add(leftItem
                    .position(CGPoint(x: sideInset + leftItem.size.width / 2.0, y: contentHeight / 2.0))
                )
                centerLeftInset += leftItem.size.width + 4.0
            }
            
            var centerRightInset = sideInset
            if let rightItem = rightItem {
                context.add(rightItem
                    .position(CGPoint(x: context.availableSize.width - sideInset - rightItem.size.width / 2.0, y: contentHeight / 2.0))
                )
                centerRightInset += rightItem.size.width + 4.0
            }
            
            let maxCenterInset = max(centerLeftInset, centerRightInset)
            if let centerItem = centerItem {
                context.add(centerItem
                    .position(CGPoint(x: maxCenterInset + (context.availableSize.width - maxCenterInset - maxCenterInset) / 2.0, y: contentHeight / 2.0))
                )
            }
            
            return size
        }
    }
}

final class RoundGradientButtonComponent: Component {
    init(gradientColors: [CGColor], icon: String? = nil, image: UIImage? = nil, title: String) {
        self.gradientColors = gradientColors
        self.icon = icon
        self.image = image
        self.title = title
    }
    
    static func == (lhs: RoundGradientButtonComponent, rhs: RoundGradientButtonComponent) -> Bool {
        if lhs.icon != rhs.icon {
            return false
        }
        if lhs.gradientColors != rhs.gradientColors {
            return false
        }
        return true
    }
    
    let gradientColors: [CGColor]
    let icon: String?
    let image: UIImage?
    let title: String
    
    final class View: UIView {
        let gradientLayer = CAGradientLayer()
        let iconView = UIImageView()
        let titleLabel = ComponentView<Empty>()
        
        override init(frame: CGRect = .zero) {
            super.init(frame: frame)
            
            gradientLayer.type = .radial
            gradientLayer.startPoint = .init(x: 1, y: 1)
            gradientLayer.endPoint = .init(x: 0, y: 0)
            
            self.layer.addSublayer(gradientLayer)
            self.addSubview(iconView)
            self.clipsToBounds = false
        }
        
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
        
        func update(component: RoundGradientButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
            self.iconView.image = component.image ?? component.icon.flatMap { UIImage(bundleImageName: $0) }
            let gradientColors: [CGColor]
            if component.gradientColors.count == 1 {
                gradientColors = [component.gradientColors[0], component.gradientColors[0]]
            } else {
                gradientColors = component.gradientColors
            }
            self.gradientLayer.colors = gradientColors
            
            let titleSize = self.titleLabel.update(
                transition: .immediate,
                component: AnyComponent(Text(text: component.title, font: Font.regular(13.0), color: .white)),
                environment: {},
                containerSize: CGSize(width: 100.0, height: 100.0)
            )
            
            let heightForIcon = availableSize.height - max(round(titleSize.height), 12) - 8.0
            self.iconView.frame = .init(x: availableSize.width * 0.5 - heightForIcon / 2, y: 0, width: heightForIcon, height: heightForIcon)
            self.gradientLayer.masksToBounds = true
            self.gradientLayer.cornerRadius = min(self.iconView.frame.width, self.iconView.frame.height) / 2
            self.gradientLayer.frame = self.iconView.frame
            
            if let titleView = self.titleLabel.view {
                if titleView.superview == nil {
                    self.addSubview(titleView)
                }
                titleView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: availableSize.height - titleSize.height), size: titleSize)
            }
                                           
            return availableSize
        }
    }
    
    func makeView() -> View {
        View()
    }
    
    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)
    }
}

public final class Throttler<T: Hashable> {
    public var duration: TimeInterval = 0.25
    public var queue: DispatchQueue = .main
    public var isEnabled: Bool { duration > 0 }
    
    private var isThrottling: Bool = false
    private var lastValue: T?
    private var accumulator = Set<T>()
    private var lastCompletedValue: T?
    
    public init(duration: TimeInterval = 0.25, queue: DispatchQueue = .main) {
        self.duration = duration
        self.queue = queue
    }
    
    public func publish(_ value: T, includingLatest: Bool = false, using completion: ((T) -> Void)?) {
        queue.async { [self] in
            accumulator.insert(value)
            
            if !isThrottling {
                isThrottling = true
                lastValue = nil
                completion?(value)
                self.lastCompletedValue = value
            } else {
                lastValue = value
            }
            
            if lastValue == nil {
                queue.asyncAfter(deadline: .now() + duration) { [self] in
                    accumulator.removeAll()
                    // TODO: quick fix, replace with timer
                    queue.asyncAfter(deadline: .now() + duration) { [self] in
                        isThrottling = false
                    }
                    
                    guard
                        let lastValue = lastValue,
                        lastCompletedValue != lastValue || includingLatest
                    else { return }
                    
                    accumulator.insert(lastValue)
                    self.lastValue = nil
                    completion?(lastValue)
                    lastCompletedValue = lastValue
                }
            }
        }
    }
    
    public func cancelCurrent() {
        lastValue = nil
        isThrottling = false
        accumulator.removeAll()
    }
    
    public func canEmit(_ value: T) -> Bool {
        !accumulator.contains(value)
    }
}

public extension Throttler where T == Bool {
    func throttle(includingLatest: Bool = false, _ completion: ((T) -> Void)?) {
        publish(true, includingLatest: includingLatest, using: completion)
    }
}