// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.

import UIKit
import MediaPlayer
import WebRTC
import SessionUIKit
import SessionMessagingKit
import SessionUtilitiesKit

final class CallVC: UIViewController, VideoPreviewDelegate {
    let call: SessionCall
    var latestKnownAudioOutputDeviceName: String?
    var durationTimer: Timer?
    var duration: Int = 0
    var shouldRestartCamera = true
    weak var conversationVC: ConversationVC? = nil
    
    lazy var cameraManager: CameraManager = {
        let result = CameraManager()
        result.delegate = self
        return result
    }()
    
    // MARK: - UI Components
    
    private lazy var localVideoView: LocalVideoView = {
        let result = LocalVideoView()
        result.clipsToBounds = true
        result.themeBackgroundColor = .backgroundSecondary
        result.isHidden = !call.isVideoEnabled
        result.layer.cornerRadius = UIDevice.current.isIPad ? 20 : 10
        result.layer.masksToBounds = true
        result.set(.width, to: LocalVideoView.width)
        result.set(.height, to: LocalVideoView.height)
        result.makeViewDraggable()
        
        return result
    }()
    
    private lazy var remoteVideoView: RemoteVideoView = {
        let result = RemoteVideoView()
        result.alpha = 0
        result.themeBackgroundColor = .backgroundPrimary
        result.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleRemoteVieioViewTapped)))
        
        return result
    }()
    
    private lazy var fadeView: GradientView = {
        let height: CGFloat = ((UIApplication.shared.keyWindow?.safeAreaInsets.top)
            .map { $0 + Values.veryLargeSpacing })
            .defaulting(to: 64)

        let result: GradientView = GradientView()
        result.themeBackgroundGradient = [
            .value(.backgroundPrimary, alpha: 0.4),
            .value(.backgroundPrimary, alpha: 0)
        ]
        result.set(.height, to: height)

        return result
    }()
    
    private lazy var profilePictureView: UIImageView = {
        let result = UIImageView()
        let radius: CGFloat = isIPhone6OrSmaller ? 100 : 120
        result.image = self.call.profilePicture
        result.set(.width, to: radius * 2)
        result.set(.height, to: radius * 2)
        result.layer.cornerRadius = radius
        result.layer.masksToBounds = true
        result.contentMode = .scaleAspectFill
        
        return result
    }()
    
    private lazy var minimizeButton: UIButton = {
        let result = UIButton(type: .custom)
        result.setImage(
            UIImage(named: "Minimize")?
                .withRenderingMode(.alwaysTemplate),
            for: .normal
        )
        result.themeTintColor = .textPrimary
        result.addTarget(self, action: #selector(minimize), for: UIControl.Event.touchUpInside)
        
        result.isHidden = !call.hasConnected
        result.set(.width, to: 60)
        result.set(.height, to: 60)
        
        return result
    }()
    
    private lazy var answerButton: UIButton = {
        let result = UIButton(type: .custom)
        result.setImage(
            UIImage(named: "AnswerCall")?
                .withRenderingMode(.alwaysTemplate),
            for: .normal
        )
        result.themeTintColor = .white
        result.themeBackgroundColor = .callAccept_background
        result.layer.cornerRadius = 30
        result.addTarget(self, action: #selector(answerCall), for: UIControl.Event.touchUpInside)
        
        result.isHidden = call.hasStartedConnecting
        result.set(.width, to: 60)
        result.set(.height, to: 60)
        
        return result
    }()
    
    private lazy var hangUpButton: UIButton = {
        let result = UIButton(type: .custom)
        result.setImage(
            UIImage(named: "EndCall")?
                .withRenderingMode(.alwaysTemplate),
            for: .normal
        )
        result.themeTintColor = .white
        result.themeBackgroundColor = .callDecline_background
        result.layer.cornerRadius = 30
        result.addTarget(self, action: #selector(endCall), for: UIControl.Event.touchUpInside)
        result.set(.width, to: 60)
        result.set(.height, to: 60)
        
        return result
    }()
    
    private lazy var responsePanel: UIStackView = {
        let result = UIStackView(arrangedSubviews: [hangUpButton, answerButton])
        result.axis = .horizontal
        result.spacing = Values.veryLargeSpacing * 2 + 40
        
        return result
    }()

    private lazy var switchCameraButton: UIButton = {
        let result = UIButton(type: .custom)
        result.isEnabled = call.isVideoEnabled
        result.setImage(
            UIImage(named: "SwitchCamera")?
                .withRenderingMode(.alwaysTemplate),
            for: .normal
        )
        result.themeTintColor = .textPrimary
        result.themeBackgroundColor = .backgroundSecondary
        result.layer.cornerRadius = 30
        result.addTarget(self, action: #selector(switchCamera), for: UIControl.Event.touchUpInside)
        result.set(.width, to: 60)
        result.set(.height, to: 60)
        
        return result
    }()

    private lazy var switchAudioButton: UIButton = {
        let result = UIButton(type: .custom)
        result.setImage(
            UIImage(named: "AudioOff")?
                .withRenderingMode(.alwaysTemplate),
            for: .normal
        )
        result.themeTintColor = (call.isMuted ?
            .white :
            .textPrimary
        )
        result.themeBackgroundColor = (call.isMuted ?
            .danger :
            .backgroundSecondary
        )
        result.layer.cornerRadius = 30
        result.addTarget(self, action: #selector(switchAudio), for: UIControl.Event.touchUpInside)
        result.set(.width, to: 60)
        result.set(.height, to: 60)
        
        return result
    }()
    
    private lazy var videoButton: UIButton = {
        let result = UIButton(type: .custom)
        result.setImage(
            UIImage(named: "VideoCall")?
                .withRenderingMode(.alwaysTemplate),
            for: .normal
        )
        result.themeTintColor = .textPrimary
        result.themeBackgroundColor = .backgroundSecondary
        result.layer.cornerRadius = 30
        result.addTarget(self, action: #selector(operateCamera), for: UIControl.Event.touchUpInside)
        result.set(.width, to: 60)
        result.set(.height, to: 60)
        
        return result
    }()
    
    private lazy var volumeView: MPVolumeView = {
        let result = MPVolumeView()
        result.showsVolumeSlider = false
        result.showsRouteButton = true
        result.setRouteButtonImage(
            UIImage(named: "Speaker")?
                .withRenderingMode(.alwaysTemplate),
            for: .normal
        )
        result.themeTintColor = .textPrimary
        result.themeBackgroundColor = .backgroundSecondary
        result.layer.cornerRadius = 30
        result.set(.width, to: 60)
        result.set(.height, to: 60)
        
        return result
    }()
    
    private lazy var operationPanel: UIStackView = {
        let result = UIStackView(arrangedSubviews: [switchCameraButton, videoButton, switchAudioButton, volumeView])
        result.axis = .horizontal
        result.spacing = Values.veryLargeSpacing
        
        return result
    }()
    
    private lazy var titleLabel: UILabel = {
        let result: UILabel = UILabel()
        result.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
        result.themeTextColor = .textPrimary
        result.textAlignment = .center
        
        return result
    }()
    
    private lazy var callInfoLabel: UILabel = {
        let result: UILabel = UILabel()
        result.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
        result.themeTextColor = .textPrimary
        result.textAlignment = .center
        result.isHidden = call.hasConnected
        
        if call.hasStartedConnecting { result.text = "Connecting..." }
        
        return result
    }()
    
    private lazy var callDurationLabel: UILabel = {
        let result = UILabel()
        result.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
        result.themeTextColor = .textPrimary
        result.textAlignment = .center
        result.isHidden = true
        
        return result
    }()
    
    // MARK: - Lifecycle
    
    init(for call: SessionCall) {
        self.call = call
        super.init(nibName: nil, bundle: nil)
        setupStateChangeCallbacks()
        self.modalPresentationStyle = .overFullScreen
        self.modalTransitionStyle = .crossDissolve
    }
    
    func setupStateChangeCallbacks() {
        self.call.remoteVideoStateDidChange = { isEnabled in
            DispatchQueue.main.async {
                UIView.animate(withDuration: 0.25) {
                    self.remoteVideoView.alpha = isEnabled ? 1 : 0
                }
                
                if self.callInfoLabel.alpha < 0.5 {
                    UIView.animate(withDuration: 0.25) {
                        self.operationPanel.alpha = 1
                        self.responsePanel.alpha = 1
                        self.callInfoLabel.alpha = 1
                    }
                }
            }
        }
        
        self.call.hasStartedConnectingDidChange = {
            DispatchQueue.main.async {
                self.callInfoLabel.text = "Connecting..."
                self.answerButton.alpha = 0
                
                UIView.animate(
                    withDuration: 0.5,
                    delay: 0,
                    usingSpringWithDamping: 1,
                    initialSpringVelocity: 1,
                    options: .curveEaseIn,
                    animations: { [weak self] in
                        self?.answerButton.isHidden = true
                    },
                    completion: nil
                )
            }
        }
        
        self.call.hasConnectedDidChange = { [weak self] in
            DispatchQueue.main.async {
                CallRingTonePlayer.shared.stopPlayingRingTone()
                
                self?.callInfoLabel.text = "Connected"
                self?.minimizeButton.isHidden = false
                self?.durationTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
                    self?.updateDuration()
                }
                self?.callInfoLabel.isHidden = true
                self?.callDurationLabel.isHidden = false
            }
        }
        
        self.call.hasEndedDidChange = { [weak self] in
            DispatchQueue.main.async {
                self?.durationTimer?.invalidate()
                self?.durationTimer = nil
                self?.handleEndCallMessage()
            }
        }
        
        self.call.hasStartedReconnecting = { [weak self] in
            DispatchQueue.main.async {
                self?.callInfoLabel.isHidden = false
                self?.callDurationLabel.isHidden = true
                self?.callInfoLabel.text = "Reconnecting..."
            }
        }
        
        self.call.hasReconnected = { [weak self] in
            DispatchQueue.main.async {
                self?.callInfoLabel.isHidden = true
                self?.callDurationLabel.isHidden = false
            }
        }
    }
    
    required init(coder: NSCoder) { preconditionFailure("Use init(for:) instead.") }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.themeBackgroundColor = .backgroundPrimary
        
        setUpViewHierarchy()
        
        if shouldRestartCamera { cameraManager.prepare() }
        
        touch(call.videoCapturer)
        titleLabel.text = self.call.contactName
        AppEnvironment.shared.callManager.startCall(call) { [weak self] error in
            DispatchQueue.main.async {
                if let _ = error {
                    self?.callInfoLabel.text = "Can't start a call."
                    self?.endCall()
                }
                else {
                    self?.callInfoLabel.text = "Ringing..."
                    self?.answerButton.isHidden = true
                }
            }
        }
        setupOrientationMonitoring()
        NotificationCenter.default.addObserver(self, selector: #selector(audioRouteDidChange), name: AVAudioSession.routeChangeNotification, object: nil)
    }
    
    deinit {
        UIDevice.current.endGeneratingDeviceOrientationNotifications()
        NotificationCenter.default.removeObserver(self)
    }
    
    func setUpViewHierarchy() {
        // Profile picture container
        let profilePictureContainer = UIView()
        view.addSubview(profilePictureContainer)
        
        // Remote video view
        call.attachRemoteVideoRenderer(remoteVideoView)
        view.addSubview(remoteVideoView)
        remoteVideoView.translatesAutoresizingMaskIntoConstraints = false
        remoteVideoView.pin(to: view)
        
        // Local video view
        call.attachLocalVideoRenderer(localVideoView)
        
        // Fade view
        view.addSubview(fadeView)
        fadeView.translatesAutoresizingMaskIntoConstraints = false
        fadeView.pin([ UIView.HorizontalEdge.left, UIView.VerticalEdge.top, UIView.HorizontalEdge.right ], to: view)
        
        // Minimize button
        view.addSubview(minimizeButton)
        minimizeButton.translatesAutoresizingMaskIntoConstraints = false
        minimizeButton.pin(.left, to: .left, of: view)
        minimizeButton.pin(.top, to: .top, of: view, withInset: 32)
        
        // Title label
        view.addSubview(titleLabel)
        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        titleLabel.center(.vertical, in: minimizeButton)
        titleLabel.center(.horizontal, in: view)
        
        // Response Panel
        view.addSubview(responsePanel)
        responsePanel.center(.horizontal, in: view)
        responsePanel.pin(.bottom, to: .bottom, of: view.safeAreaLayoutGuide, withInset: -Values.smallSpacing)
        
        // Operation Panel
        view.addSubview(operationPanel)
        operationPanel.center(.horizontal, in: view)
        operationPanel.pin(.bottom, to: .top, of: responsePanel, withInset: -Values.veryLargeSpacing)
        
        // Profile picture view
        profilePictureContainer.pin(.top, to: .bottom, of: fadeView)
        profilePictureContainer.pin(.bottom, to: .top, of: operationPanel)
        profilePictureContainer.pin([ UIView.HorizontalEdge.left, UIView.HorizontalEdge.right ], to: view)
        profilePictureContainer.addSubview(profilePictureView)
        profilePictureView.center(in: profilePictureContainer)
        
        // Call info label
        let callInfoLabelContainer = UIView()
        view.addSubview(callInfoLabelContainer)
        callInfoLabelContainer.pin(.top, to: .bottom, of: profilePictureView)
        callInfoLabelContainer.pin(.bottom, to: .bottom, of: profilePictureContainer)
        callInfoLabelContainer.pin([ UIView.HorizontalEdge.left, UIView.HorizontalEdge.right ], to: view)
        callInfoLabelContainer.addSubview(callInfoLabel)
        callInfoLabelContainer.addSubview(callDurationLabel)
        callInfoLabel.translatesAutoresizingMaskIntoConstraints = false
        callInfoLabel.center(in: callInfoLabelContainer)
        callDurationLabel.translatesAutoresizingMaskIntoConstraints = false
        callDurationLabel.center(in: callInfoLabelContainer)
    }
    
    private func addLocalVideoView() {
        let safeAreaInsets = UIApplication.shared.keyWindow?.safeAreaInsets
        CurrentAppContext().mainWindow?.addSubview(localVideoView)
        localVideoView.autoPinEdge(toSuperviewEdge: .right, withInset: Values.smallSpacing)
        let topMargin = (safeAreaInsets?.top ?? 0) + Values.veryLargeSpacing
        localVideoView.autoPinEdge(toSuperviewEdge: .top, withInset: topMargin)
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        if (call.isVideoEnabled && shouldRestartCamera) { cameraManager.start() }
        
        shouldRestartCamera = true
        addLocalVideoView()
        remoteVideoView.alpha = (call.isRemoteVideoEnabled ? 1 : 0)
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        
        if (call.isVideoEnabled && shouldRestartCamera) { cameraManager.stop() }
        
        localVideoView.removeFromSuperview()
    }
    
    // MARK: - Orientation

    private func setupOrientationMonitoring() {
        UIDevice.current.beginGeneratingDeviceOrientationNotifications()
        NotificationCenter.default.addObserver(self, selector: #selector(didChangeDeviceOrientation), name: UIDevice.orientationDidChangeNotification, object: UIDevice.current)
    }
    
    @objc func didChangeDeviceOrientation(notification: Notification) {
        if UIDevice.current.isIPad { return }

        func rotateAllButtons(rotationAngle: CGFloat) {
            let transform = CGAffineTransform(rotationAngle: rotationAngle)
            
            UIView.animate(withDuration: 0.2) {
                self.answerButton.transform = transform
                self.hangUpButton.transform = transform
                self.switchAudioButton.transform = transform
                self.switchCameraButton.transform = transform
                self.videoButton.transform = transform
                self.volumeView.transform = transform
            }
        }
        
        switch UIDevice.current.orientation {
            case .portrait: rotateAllButtons(rotationAngle: 0)
            case .portraitUpsideDown: rotateAllButtons(rotationAngle: .pi)
            case .landscapeLeft: rotateAllButtons(rotationAngle: .halfPi)
            case .landscapeRight: rotateAllButtons(rotationAngle: .pi + .halfPi)
            default: break
        }
    }
    
    // MARK: Call signalling
    func handleAnswerMessage(_ message: CallMessage) {
        callInfoLabel.text = "Connecting..."
    }
    
    func handleEndCallMessage() {
        SNLog("[Calls] Ending call.")
        self.callInfoLabel.isHidden = false
        self.callDurationLabel.isHidden = true
        self.callInfoLabel.text = "Call Ended"
        
        UIView.animate(withDuration: 0.25) {
            self.remoteVideoView.alpha = 0
            self.operationPanel.alpha = 1
            self.responsePanel.alpha = 1
            self.callInfoLabel.alpha = 1
        }
        
        Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { [weak self] _ in
            self?.conversationVC?.showInputAccessoryView()
            self?.presentingViewController?.dismiss(animated: true, completion: nil)
        }
    }
    
    @objc private func answerCall() {
        AppEnvironment.shared.callManager.answerCall(call) { [weak self] error in
            DispatchQueue.main.async {
                if let _ = error {
                    self?.callInfoLabel.text = "Can't answer the call."
                    self?.endCall()
                }
            }
        }
    }
    
    @objc private func endCall() {
        AppEnvironment.shared.callManager.endCall(call) { [weak self] error in
            if let _ = error {
                self?.call.endSessionCall()
                AppEnvironment.shared.callManager.reportCurrentCallEnded(reason: nil)
            }
            
            DispatchQueue.main.async {
                self?.conversationVC?.showInputAccessoryView()
                self?.presentingViewController?.dismiss(animated: true, completion: nil)
            }
        }
    }
    
    @objc private func updateDuration() {
        callDurationLabel.text = String(format: "%.2d:%.2d", duration/60, duration%60)
        duration += 1
    }
    
    // MARK: - Minimize to a floating view
    
    @objc private func minimize() {
        self.shouldRestartCamera = false
        self.conversationVC?.showInputAccessoryView()
        
        let miniCallView = MiniCallView(from: self)
        miniCallView.show()
        
        presentingViewController?.dismiss(animated: true, completion: nil)
    }
    
    // MARK: - Video and Audio
    
    @objc private func operateCamera() {
        if (call.isVideoEnabled) {
            localVideoView.isHidden = true
            cameraManager.stop()
            videoButton.themeTintColor = .textPrimary
            videoButton.themeBackgroundColor = .backgroundSecondary
            switchCameraButton.isEnabled = false
            call.isVideoEnabled = false
        }
        else {
            guard Permissions.requestCameraPermissionIfNeeded() else { return }
            let previewVC = VideoPreviewVC()
            previewVC.delegate = self
            present(previewVC, animated: true, completion: nil)
        }
    }
    
    func cameraDidConfirmTurningOn() {
        localVideoView.isHidden = false
        cameraManager.prepare()
        cameraManager.start()
        videoButton.themeTintColor = .backgroundSecondary
        videoButton.themeBackgroundColor = .textPrimary
        switchCameraButton.isEnabled = true
        call.isVideoEnabled = true
    }
    
    @objc private func switchCamera() {
        cameraManager.switchCamera()
    }
    
    @objc private func switchAudio() {
        if call.isMuted {
            switchAudioButton.themeTintColor = .textPrimary
            switchAudioButton.themeBackgroundColor = .backgroundSecondary
            call.isMuted = false
        }
        else {
            switchAudioButton.themeTintColor = .white
            switchAudioButton.themeBackgroundColor = .danger
            call.isMuted = true
        }
    }
    
    @objc private func audioRouteDidChange() {
        let currentSession = AVAudioSession.sharedInstance()
        let currentRoute = currentSession.currentRoute
        if let currentOutput = currentRoute.outputs.first {
            if let latestKnownAudioOutputDeviceName = latestKnownAudioOutputDeviceName, currentOutput.portName == latestKnownAudioOutputDeviceName { return }
            
            latestKnownAudioOutputDeviceName = currentOutput.portName
            
            switch currentOutput.portType {
                case .builtInSpeaker:
                    let image = UIImage(named: "Speaker")?.withRenderingMode(.alwaysTemplate)
                    volumeView.setRouteButtonImage(image, for: .normal)
                    volumeView.themeTintColor = .backgroundSecondary
                    volumeView.themeBackgroundColor = .textPrimary
                    
                case .headphones:
                    let image = UIImage(named: "Headsets")?.withRenderingMode(.alwaysTemplate)
                    volumeView.setRouteButtonImage(image, for: .normal)
                    volumeView.themeTintColor = .backgroundSecondary
                    volumeView.themeBackgroundColor = .textPrimary
                    
                case .bluetoothLE: fallthrough
                case .bluetoothA2DP:
                    let image = UIImage(named: "Bluetooth")?.withRenderingMode(.alwaysTemplate)
                    volumeView.setRouteButtonImage(image, for: .normal)
                    volumeView.themeTintColor = .backgroundSecondary
                    volumeView.themeBackgroundColor = .textPrimary
                    
                case .bluetoothHFP:
                    let image = UIImage(named: "Airpods")?.withRenderingMode(.alwaysTemplate)
                    volumeView.setRouteButtonImage(image, for: .normal)
                    volumeView.themeTintColor = .backgroundSecondary
                    volumeView.themeBackgroundColor = .textPrimary
                    
                case .builtInReceiver: fallthrough
                default:
                    let image = UIImage(named: "Speaker")?.withRenderingMode(.alwaysTemplate)
                    volumeView.setRouteButtonImage(image, for: .normal)
                    volumeView.themeTintColor = .backgroundSecondary
                    volumeView.themeBackgroundColor = .textPrimary
            }
        }
    }
    
    @objc private func handleRemoteVieioViewTapped(gesture: UITapGestureRecognizer) {
        let isHidden = callDurationLabel.alpha < 0.5
        
        UIView.animate(withDuration: 0.5) {
            self.operationPanel.alpha = isHidden ? 1 : 0
            self.responsePanel.alpha = isHidden ? 1 : 0
            self.callDurationLabel.alpha = isHidden ? 1 : 0
        }
    }
}
