// 
// Copyright 2020 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import UIKit

/// The number of milliseconds in one second.
private let MSEC_PER_SEC: TimeInterval = 1000

@objcMembers
class RoomDirectCallStatusCell: RoomCallBaseCell {
    
    private static var className: String {
        return String(describing: self)
    }
    
    /// Action identifier used when the user pressed "Call back" button for a declined call.
    /// The `userInfo` dictionary contains an `MXEvent` object under the `kMXKRoomBubbleCellEventKey` key, representing the invite event of the declined call.
    static var callBackAction: String {
        return self.className + ".callBack"
    }
    
    /// Action identifier used when the user pressed "Answer" button for an incoming call.
    /// The `userInfo` dictionary contains an `MXEvent` object under the `kMXKRoomBubbleCellEventKey` key, representing the invite event of the call.
    static var answerAction: String {
        return self.className + ".answer"
    }
    
    /// Action identifier used when the user pressed "Decline" button for an incoming call.
    /// The `userInfo` dictionary contains an `MXEvent` object under the `kMXKRoomBubbleCellEventKey` key, representing the invite event of the call.
    static var declineAction: String {
        return self.className + ".decline"
    }
    
    /// Action identifier used when the user pressed "End call" button for an incoming call.
    /// The `userInfo` dictionary contains an `MXEvent` object under the `kMXKRoomBubbleCellEventKey` key, representing the invite event of the call.
    static var endCallAction: String {
        return self.className + ".endCall"
    }
    
    private var callDurationString: String = ""
    private var isVideoCall: Bool = false
    private var isIncoming: Bool = false
    private var callInviteEvent: MXEvent?
    private var viewState: ViewState = .unknown {
        didSet {
            updateBottomContentView()
            updateCallIcon()
        }
    }
    
    private enum ViewState {
        case unknown
        case ringing
        case active
        case declined
        case missed
        case ended
        case failed
    }
    
    private static var callDurationFormatter: DateComponentsFormatter {
        let formatter = DateComponentsFormatter()
        formatter.zeroFormattingBehavior = .dropAll
        formatter.allowedUnits = [.hour, .minute, .second]
        formatter.unitsStyle = .abbreviated
        return formatter
    }
    
    private func updateCallIcon() {
        switch viewState {
        case .missed:
            innerContentView.callIconView.image = isVideoCall ?
                Asset.Images.callMissedVideo.image :
                Asset.Images.callMissedVoice.image
        default:
            innerContentView.callIconView.image = isVideoCall ?
                Asset.Images.callVideoIcon.image :
                Asset.Images.voiceCallHangonIcon.image
        }
    }
    
    private func updateBottomContentView() {
        bottomContentView = bottomView(for: viewState)
    }
    
    private var callButtonIcon: UIImage {
        if isVideoCall {
            return Asset.Images.callVideoIcon.image
        } else {
            return Asset.Images.voiceCallHangonIcon.image
        }
    }
    
    private var actionUserInfo: [AnyHashable: Any]? {
        if let event = callInviteEvent {
            return [kMXKRoomBubbleCellEventKey: event]
        }
        return nil
    }
    
    private func bottomView(for state: ViewState) -> UIView? {
        switch state {
        case .unknown:
            return nil
        case .ringing:
            let view = HorizontalButtonsContainerView.loadFromNib()
            
            view.firstButton.style = .negative
            view.firstButton.setTitle(VectorL10n.eventFormatterCallDecline, for: .normal)
            view.firstButton.setImage(Asset.Images.voiceCallHangupIcon.image, for: .normal)
            view.firstButton.removeTarget(nil, action: nil, for: .touchUpInside)
            view.firstButton.addTarget(self, action: #selector(declineCallAction(_:)), for: .touchUpInside)
            
            view.secondButton.style = .positive
            view.secondButton.setTitle(VectorL10n.eventFormatterCallAnswer, for: .normal)
            view.secondButton.setImage(callButtonIcon, for: .normal)
            view.secondButton.removeTarget(nil, action: nil, for: .touchUpInside)
            view.secondButton.addTarget(self, action: #selector(answerCallAction(_:)), for: .touchUpInside)
            
            return view
        case .active:
            let view = HorizontalButtonsContainerView.loadFromNib()
            view.secondButton.isHidden = true
            
            view.firstButton.style = .negative
            view.firstButton.setTitle(VectorL10n.eventFormatterCallEndCall, for: .normal)
            view.firstButton.setImage(Asset.Images.voiceCallHangupIcon.image, for: .normal)
            view.firstButton.removeTarget(nil, action: nil, for: .touchUpInside)
            view.firstButton.addTarget(self, action: #selector(endCallAction(_:)), for: .touchUpInside)
            
            return view
        case .declined:
            let view = HorizontalButtonsContainerView.loadFromNib()
            view.secondButton.isHidden = true
            
            view.firstButton.style = .positive
            view.firstButton.setTitle(VectorL10n.eventFormatterCallBack, for: .normal)
            view.firstButton.setImage(callButtonIcon, for: .normal)
            view.firstButton.removeTarget(nil, action: nil, for: .touchUpInside)
            view.firstButton.addTarget(self, action: #selector(callBackAction(_:)), for: .touchUpInside)
            
            return view
        case .missed:
            let view = HorizontalButtonsContainerView.loadFromNib()
            view.secondButton.isHidden = true
            
            view.firstButton.style = .positive
            view.firstButton.setTitle(VectorL10n.eventFormatterCallBack, for: .normal)
            view.firstButton.setImage(callButtonIcon, for: .normal)
            view.firstButton.removeTarget(nil, action: nil, for: .touchUpInside)
            view.firstButton.addTarget(self, action: #selector(callBackAction(_:)), for: .touchUpInside)
            
            return view
        case .ended:
            return nil
        case .failed:
            let view = HorizontalButtonsContainerView.loadFromNib()
            view.secondButton.isHidden = true
            
            view.firstButton.style = .positive
            view.firstButton.setTitle(VectorL10n.eventFormatterCallRetry, for: .normal)
            view.firstButton.setImage(callButtonIcon, for: .normal)
            view.firstButton.removeTarget(nil, action: nil, for: .touchUpInside)
            view.firstButton.addTarget(self, action: #selector(callBackAction(_:)), for: .touchUpInside)
            
            return view
        }
    }
    
    private func configure(withCall call: MXCall) {
        switch call.state {
        case .fledgling,
            .waitLocalMedia,
            .createOffer,
            .connecting:
            viewState = .active
            if call.isIncoming {
                statusText = isVideoCall ? VectorL10n.eventFormatterCallActiveVideo : VectorL10n.eventFormatterCallActiveVoice
            } else {
                statusText = VectorL10n.eventFormatterCallConnecting
            }
        case .inviteSent:
            if call.isIncoming {
                statusText = isVideoCall ? VectorL10n.eventFormatterCallActiveVideo : VectorL10n.eventFormatterCallActiveVoice
            } else {
                statusText = VectorL10n.eventFormatterCallRinging
            }
        case .createAnswer,
             .connected,
             .onHold,
             .remotelyOnHold:
            viewState = .active
            statusText = isVideoCall ? VectorL10n.eventFormatterCallActiveVideo : VectorL10n.eventFormatterCallActiveVoice
        case .ringing:
            if call.isIncoming {
                viewState = .ringing
                statusText = isVideoCall ? VectorL10n.eventFormatterCallIncomingVideo : VectorL10n.eventFormatterCallIncomingVoice
            } else {
                viewState = .active
                statusText = isVideoCall ? VectorL10n.eventFormatterCallActiveVideo : VectorL10n.eventFormatterCallActiveVoice
            }
        case .ended:
            switch call.endReason {
            case .unknown,
                 .hangup,
                 .hangupElsewhere,
                 .remoteHangup,
                 .answeredElseWhere:
                viewState = .ended
                updateStatusTextForEndedCall()
            case .missed:
                if call.isIncoming {
                    viewState = .missed
                    statusText = isVideoCall ? VectorL10n.eventFormatterCallMissedVideo : VectorL10n.eventFormatterCallMissedVoice
                } else {
                    updateStatusTextForEndedCall()
                }
            case .busy:
                configureForRejectedCall(call: call)
            @unknown default:
                viewState = .ended
                updateStatusTextForEndedCall()
            }
        case .inviteExpired,
             .answeredElseWhere:
            viewState = .ended
            updateStatusTextForEndedCall()
        @unknown default:
            viewState = .ended
            updateStatusTextForEndedCall()
        }
    }
    
    private func configureForRejectedCall(withEvent event: MXEvent? = nil, call: MXCall? = nil, bubbleCellData: RoomBubbleCellData? = nil) {
        
        let isMyReject: Bool
        
        if let call = call, call.isIncoming {
            isMyReject = true
        } else if let event = event, let bubbleCellData = bubbleCellData, event.sender == bubbleCellData.mxSession.myUserId {
            isMyReject = true
        } else {
            isMyReject = false
        }
        
        if isMyReject {
            viewState = .declined
            statusText = VectorL10n.eventFormatterCallYouDeclined
        } else {
            viewState = .ended
            updateStatusTextForEndedCall()
        }
    }
    
    private func configureForHangupCall(withEvent event: MXEvent) {
        guard let hangupEventContent = MXCallHangupEventContent(fromJSON: event.content) else {
            viewState = .ended
            updateStatusTextForEndedCall()
            return
        }
        
        switch hangupEventContent.reasonType {
        case .userHangup:
            viewState = .ended
            updateStatusTextForEndedCall()
        default:
            viewState = .failed
            statusText = VectorL10n.eventFormatterCallConnectionFailed
        }
    }
    
    private func configureForUnansweredCall() {
        if isIncoming {
            //  missed call
            viewState = .missed
            statusText = isVideoCall ? VectorL10n.eventFormatterCallMissedVideo : VectorL10n.eventFormatterCallMissedVoice
        } else {
            //  outgoing unanswered call
            viewState = .ended
            updateStatusTextForEndedCall()
        }
    }
    
    private func updateStatusTextForEndedCall() {
        if callDurationString.count > 0 {
            statusText = VectorL10n.eventFormatterCallHasEndedWithTime(callDurationString)
        } else {
            statusText = VectorL10n.eventFormatterCallHasEnded
        }
    }
    
    //  MARK: - Actions
    
    @objc
    private func callBackAction(_ sender: CallTileActionButton) {
        self.delegate?.cell(self,
                            didRecognizeAction: Self.callBackAction,
                            userInfo: actionUserInfo)
    }
    
    @objc
    private func declineCallAction(_ sender: CallTileActionButton) {
        self.delegate?.cell(self,
                            didRecognizeAction: Self.declineAction,
                            userInfo: actionUserInfo)
    }
    
    @objc
    private func answerCallAction(_ sender: CallTileActionButton) {
        self.delegate?.cell(self,
                            didRecognizeAction: Self.answerAction,
                            userInfo: actionUserInfo)
    }
    
    @objc
    private func endCallAction(_ sender: CallTileActionButton) {
        self.delegate?.cell(self,
                            didRecognizeAction: Self.endCallAction,
                            userInfo: actionUserInfo)
    }
    
    //  MARK: - MXKCellRendering
    
    override func render(_ cellData: MXKCellData!) {
        super.render(cellData)
        
        viewState = .unknown
        
        guard let bubbleCellData = cellData as? RoomBubbleCellData else {
            return
        }
        
        let events = bubbleCellData.allLinkedEvents()
        
        guard let inviteEvent = events.first(where: { $0.eventType == .callInvite }) else {
            return
        }
        
        if bubbleCellData.senderId == bubbleCellData.mxSession.myUserId {
            //  event sent by my user, no means in displaying our own avatar and display name
            if let directUserId = bubbleCellData.mxSession.directUserId(inRoom: bubbleCellData.roomId) {
                let user = bubbleCellData.mxSession.user(withUserId: directUserId)
                
                let placeholder = AvatarGenerator.generateAvatar(forMatrixItem: directUserId,
                                                                 withDisplayName: user?.displayname)
                
                innerContentView.avatarImageView.setImageURI(user?.avatarUrl,
                                            withType: nil,
                                            andImageOrientation: .up,
                                            toFitViewSize: innerContentView.avatarImageView.frame.size,
                                            with: MXThumbnailingMethodCrop,
                                            previewImage: placeholder,
                                            mediaManager: bubbleCellData.mxSession.mediaManager)
                innerContentView.avatarImageView.defaultBackgroundColor = .clear
                
                innerContentView.callerNameLabel.text = user?.displayname
            }
        } else {
            innerContentView.avatarImageView.setImageURI(bubbleCellData.senderAvatarUrl,
                                        withType: nil,
                                        andImageOrientation: .up,
                                        toFitViewSize: innerContentView.avatarImageView.frame.size,
                                        with: MXThumbnailingMethodCrop,
                                        previewImage: bubbleCellData.senderAvatarPlaceholder,
                                        mediaManager: bubbleCellData.mxSession.mediaManager)
            innerContentView.avatarImageView.defaultBackgroundColor = .clear
            
            innerContentView.callerNameLabel.text = bubbleCellData.senderDisplayName
        }
        
        guard let callInviteEventContent = MXCallInviteEventContent(fromJSON: inviteEvent.content) else {
            return
        }
        isVideoCall = callInviteEventContent.isVideoCall()
        callDurationString = readableCallDuration(from: events)
        isIncoming = inviteEvent.sender != bubbleCellData.mxSession.myUserId
        callInviteEvent = inviteEvent
        updateCallIcon()
        let callId = callInviteEventContent.callId
        guard let call = bubbleCellData.mxSession.callManager.call(withCallId: callId) else {
            
            //  check events include a reject event
            if let rejectEvent = events.first(where: { $0.eventType == .callReject }) {
                configureForRejectedCall(withEvent: rejectEvent, bubbleCellData: bubbleCellData)
                return
            }
            
            //  check events include an answer event
            if !events.contains(where: { $0.eventType == .callAnswer }) {
                configureForUnansweredCall()
                return
            }
            
            //  check events include a hangup event
            if let hangupEvent = events.first(where: { $0.eventType == .callHangup }) {
                configureForHangupCall(withEvent: hangupEvent)
                return
            }
            
            //  there is no reject or hangup event, we can just say this call has ended
            viewState = .ended
            updateStatusTextForEndedCall()
            return
        }
        
        configure(withCall: call)
    }
    
    private func callDuration(from events: [MXEvent]) -> TimeInterval {
        guard let startDate = events.first(where: { $0.eventType == .callAnswer })?.originServerTs else {
            //  never started
            return 0
        }
        guard let endDate = events.first(where: { $0.eventType == .callHangup })?.originServerTs
                ?? events.first(where: { $0.eventType == .callReject })?.originServerTs else {
            //  not ended yet, compute the diff from now
            return (NSTimeIntervalSince1970 - TimeInterval(startDate))/MSEC_PER_SEC
        }
        
        guard startDate < endDate else {
            // started but hung up/rejected on other end around the same time
            return 0
        }
        
        //  ended, compute the diff between two dates
        return TimeInterval(endDate - startDate)/MSEC_PER_SEC
    }
    
    private func readableCallDuration(from events: [MXEvent]) -> String {
        let duration = callDuration(from: events)
        
        if duration <= 0 {
            return ""
        }
        
        return RoomDirectCallStatusCell.callDurationFormatter.string(from: duration) ?? ""
    }
    
}
