//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import SignalServiceKit
import SignalUI

public class CVAccessibilityCustomAction: UIAccessibilityCustomAction {
    public var messageAction: MessageAction?
}

extension ConversationViewController: UIGestureRecognizerDelegate {
    func createGestureRecognizers() {
        collectionViewTapGestureRecognizer.setTapDelegate(self)
        collectionViewTapGestureRecognizer.delegate = self
        collectionView.addGestureRecognizer(collectionViewTapGestureRecognizer)

        collectionViewLongPressGestureRecognizer.addTarget(self, action: #selector(handleLongPressGesture))
        collectionViewLongPressGestureRecognizer.delegate = self
        collectionView.addGestureRecognizer(collectionViewLongPressGestureRecognizer)

        collectionViewContextMenuGestureRecognizer.addTarget(self, action: #selector(handleLongPressGesture))
        collectionViewContextMenuGestureRecognizer.minimumPressDuration = 0.2
        collectionViewContextMenuGestureRecognizer.delegate = self
        collectionView.addGestureRecognizer(collectionViewContextMenuGestureRecognizer)

        let collectionViewContextMenuSecondaryClickRecognizer = UITapGestureRecognizer()
        collectionViewContextMenuSecondaryClickRecognizer.addTarget(self, action: #selector(handleSecondaryClickGesture))
        collectionViewContextMenuSecondaryClickRecognizer.buttonMaskRequired = [.secondary]
        collectionViewContextMenuSecondaryClickRecognizer.delegate = self
        collectionView.addGestureRecognizer(collectionViewContextMenuSecondaryClickRecognizer)
        self.collectionViewContextMenuSecondaryClickRecognizer = collectionViewContextMenuSecondaryClickRecognizer

        collectionViewPanGestureRecognizer.addTarget(self, action: #selector(handlePanGesture))
        collectionViewPanGestureRecognizer.delegate = self
        collectionView.addGestureRecognizer(collectionViewPanGestureRecognizer)

        collectionViewTapGestureRecognizer.require(toFail: collectionViewPanGestureRecognizer)
        collectionViewTapGestureRecognizer.require(toFail: collectionViewLongPressGestureRecognizer)

        // Allow panning with trackpad
        collectionViewPanGestureRecognizer.allowedScrollTypesMask = .continuous

        // There are cases where we don't have a navigation controller, such as if we got here through 3d touch.
        // Make sure we only register the gesture interaction if it actually exists. This helps the swipe back
        // gesture work reliably without conflict with audio scrubbing or swipe-to-repy.
        if let interactivePopGestureRecognizer = navigationController?.interactivePopGestureRecognizer {
            collectionViewPanGestureRecognizer.require(toFail: interactivePopGestureRecognizer)
        }
    }

    // TODO: Revisit
    private func cellAtPoint(_ point: CGPoint) -> CVCell? {
        guard let indexPath = collectionView.indexPathForItem(at: point),
              let cell = collectionView.cellForItem(at: indexPath) else { return nil }
        return cell as? CVCell
    }

    private func cellForInteractionId(_ interactionId: String) -> CVCell? {
        // TODO: Won't this build a new cell in some cases?
        guard let indexPath = indexPath(forInteractionUniqueId: interactionId),
              let cell = collectionView.cellForItem(at: indexPath) else { return nil }
        return cell as? CVCell
    }

    // MARK: - UIGestureRecognizerDelegate

    public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        guard !isShowingSelectionUI else {
            return gestureRecognizer == collectionViewTapGestureRecognizer
        }

        if gestureRecognizer == collectionViewPanGestureRecognizer {
            // Only allow the pan gesture to recognize horizontal panning,
            // to avoid conflicts with the conversation view scroll view.
            let translation = collectionViewPanGestureRecognizer.translation(in: view)
            return abs(translation.x) > abs(translation.y)
        } else {
            return true
        }
    }

    public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        // Support standard long press recognizing for body text cases, and context menu long press recognizing for everything else
        let currentIsLongPressOrTap = (gestureRecognizer == collectionViewLongPressGestureRecognizer || gestureRecognizer == collectionViewContextMenuGestureRecognizer || gestureRecognizer == collectionViewTapGestureRecognizer)
        let otherIsLongPressOrTap = (otherGestureRecognizer == collectionViewLongPressGestureRecognizer || otherGestureRecognizer == collectionViewContextMenuGestureRecognizer || otherGestureRecognizer == collectionViewTapGestureRecognizer)
        return currentIsLongPressOrTap && otherIsLongPressOrTap
    }

    public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive event: UIEvent) -> Bool {
        if collectionViewContextMenuSecondaryClickRecognizer == gestureRecognizer {
            return event.buttonMask == .secondary
        }

        return true
    }

    // MARK: -

    private func findCell(forGesture sender: UIGestureRecognizer) -> CVCell? {
        // Collection view is a scroll view; we want to ignore
        // cells that are scrolled offscreen.  So we first check
        // that the collection view contains the gesture location.
        guard collectionView.containsGestureLocation(sender) else {
            return nil
        }

        for cell in collectionView.visibleCells {
            guard let cell = cell as? CVCell else {
                owsFailDebug("Invalid cell")
                continue
            }
            guard cell.containsGestureLocation(sender) else {
                continue
            }
            return cell
        }
        return nil
    }
}

extension ConversationViewController: SingleOrDoubleTapGestureDelegate {

    // MARK: - Tap

    public func handleSingleTap(_ sender: SingleOrDoubleTapGestureRecognizer) -> Bool {
        // Stop any recording voice memos.
        finishRecordingVoiceMessage(sendImmediately: false)

        guard let cell = findCell(forGesture: sender) else {
            return false
        }

        if let interaction = collectionViewActiveContextMenuInteraction, interaction.contextMenuVisible {
            return false
        }

        return cell.handleTap(sender: sender, componentDelegate: componentDelegate)
    }

    public func handleDoubleTap(_ sender: SingleOrDoubleTapGestureRecognizer) -> Bool {
        guard let cell = findCell(forGesture: sender) else {
            return false
        }
        guard cell.canHandleDoubleTap(sender: sender, componentDelegate: self) else {
            return false
        }

        return cell.handleDoubleTap(sender: sender, componentDelegate: self)
    }

    public func didEndGesture(_ sender: SingleOrDoubleTapGestureRecognizer, wasHandled: Bool) {
        if !wasHandled {
            dismissKeyBoard()
        }
    }
}

extension ConversationViewController {

    // MARK: - Long Press

    @objc
    func handleLongPressGesture(_ sender: UILongPressGestureRecognizer) {

        let resetLongPress = {
            self.longPressHandler = nil
            sender.isEnabled = false
            sender.isEnabled = true
        }

        switch sender.state {
        case .began:
            guard let longPressHandler = findLongPressHandler(sender: sender) else {
                resetLongPress()
                return
            }
            self.longPressHandler = longPressHandler
        case .changed:
            self.longPressHandler?.handleLongPress(sender)
        case .ended, .failed, .cancelled, .possible:
            self.longPressHandler?.handleLongPress(sender)
            resetLongPress()
        @unknown default:
            owsFailDebug("Invalid state.")
        }
    }

    @objc
    func handleSecondaryClickGesture(_ sender: UITapGestureRecognizer) {
        guard let cell = findCell(forGesture: sender) else {
            return
        }
        guard let longPressHandler = cell.findLongPressHandler(sender: sender,
                                                               componentDelegate: componentDelegate) else {
            return
        }

        longPressHandler.startContextMenuGesture(cell: cell)
    }

    private func findLongPressHandler(sender: UILongPressGestureRecognizer) -> CVLongPressHandler? {
        guard let cell = findCell(forGesture: sender) else {
            return nil
        }
        guard let longPressHandler = cell.findLongPressHandler(sender: sender,
                                                               componentDelegate: componentDelegate) else {
            return nil
        }
        if sender == collectionViewContextMenuGestureRecognizer {
            longPressHandler.startContextMenuGesture(cell: cell)
        } else {
            longPressHandler.startGesture(cell: cell)
        }
        return longPressHandler
    }

    // MARK: - VoiceOver Custom Actions

    func updateAccessibilityCustomActionsForCell(cell: CVCell) {
        guard let renderItem = cell.renderItem else {
            return
        }

        let itemViewModel = CVItemViewModelImpl(renderItem: renderItem)
        let shouldAllowReply = shouldAllowReplyForItem(itemViewModel)
        let messageActions: [MessageAction]
        if itemViewModel.messageCellType == .systemMessage {
            messageActions = MessageActions.infoMessageActions(itemViewModel: itemViewModel,
                                                               delegate: self)
        } else if itemViewModel.messageCellType == .stickerMessage || itemViewModel.messageCellType == .genericAttachment {
            messageActions = MessageActions.mediaActions(itemViewModel: itemViewModel,
                                                         shouldAllowReply: shouldAllowReply,
                                                         delegate: self)
        } else {
            messageActions = MessageActions.textActions(itemViewModel: itemViewModel,
                                                        shouldAllowReply: shouldAllowReply,
                                                        delegate: self)
        }

        var actions: [CVAccessibilityCustomAction] = []
        for messageAction in messageActions {
            let action = CVAccessibilityCustomAction(
                name: messageAction.accessibilityLabel ?? messageAction.accessibilityIdentifier,
                target: self,
                selector: #selector(handleCustomAccessibilityActionInvoked(sender:))
            )
            action.messageAction = messageAction
            actions.append(action)
        }
        cell.accessibilityCustomActions = actions
    }

    @objc
    public func handleCustomAccessibilityActionInvoked(sender: UIAccessibilityCustomAction) {
        guard let cvCustomAction = sender as? CVAccessibilityCustomAction else {
            return
        }

        cvCustomAction.messageAction?.block(self)
    }

    // MARK: - Pan

    @objc
    func handlePanGesture(_ sender: UIPanGestureRecognizer) {
        let resetPan = {
            self.panHandler = nil
            sender.isEnabled = false
            sender.isEnabled = true
        }

        let updatePanGesture = {
            guard let panHandler = self.panHandler else {
                return
            }
            // The pan needs to operate on the current cell for this interaction.
            guard let cell = self.cellForInteractionId(panHandler.interactionId) else {
                owsFailDebug("No cell for pan.")
                resetPan()
                return
            }
            let messageSwipeActionState = self.viewState.messageSwipeActionState
            panHandler.handlePan(sender: sender,
                                 cell: cell,
                                 messageSwipeActionState: messageSwipeActionState)
        }

        switch sender.state {
        case .began:
            guard let panHandler = findPanHandler(sender: sender) else {
                resetPan()
                return
            }
            self.panHandler = panHandler
            startPanHandler(sender: sender)
        case .changed:
            updatePanGesture()
        case .ended, .failed, .cancelled, .possible:
            updatePanGesture()
            resetPan()
        @unknown default:
            owsFailDebug("Invalid state.")
        }
    }

    private func findPanHandler(sender: UIPanGestureRecognizer) -> CVPanHandler? {
        guard let cell = findCell(forGesture: sender) else {
            return nil
        }
        let messageSwipeActionState = viewState.messageSwipeActionState
        guard let panHandler = cell.findPanHandler(sender: sender,
                                                   componentDelegate: componentDelegate,
                                                   messageSwipeActionState: messageSwipeActionState) else {
            return nil
        }
        return panHandler
    }

    private func startPanHandler(sender: UIPanGestureRecognizer) {
        guard let panHandler = panHandler else { return }
        guard let cell = findCell(forGesture: sender) else { return }
        panHandler.startGesture(sender: sender, cell: cell, messageSwipeActionState: viewState.messageSwipeActionState)
    }
}

// MARK: -

public struct CVLongPressHandler {
    private weak var delegate: CVComponentDelegate?
    let renderItem: CVRenderItem
    let itemViewModel: CVItemViewModelImpl

    enum GestureLocation {
        case `default`
        case media
        case sticker
        case quotedReply
        case systemMessage
        case paymentMessage
        case bodyText(item: CVTextLabel.Item)
    }
    let gestureLocation: GestureLocation

    init(delegate: CVComponentDelegate,
         renderItem: CVRenderItem,
         gestureLocation: GestureLocation) {
        self.delegate = delegate
        self.renderItem = renderItem
        self.gestureLocation = gestureLocation

        // TODO: shouldAutoUpdate?
        self.itemViewModel = CVItemViewModelImpl(renderItem: renderItem)
    }

    func startContextMenuGesture(cell: CVCell) {
        guard let delegate = self.delegate else {
            owsFailDebug("Missing delegate.")
            return
        }

        let shouldAllowReply = delegate.shouldAllowReplyForItem(itemViewModel)

        switch gestureLocation {
        case .`default`:
            // TODO: Rename from "Text view item" to "default"?
            delegate.didLongPressTextViewItem(cell,
                                              itemViewModel: itemViewModel,
                                              shouldAllowReply: shouldAllowReply)
        case .media:
            delegate.didLongPressMediaViewItem(cell,
                                               itemViewModel: itemViewModel,
                                               shouldAllowReply: shouldAllowReply)
        case .sticker:
            delegate.didLongPressSticker(cell,
                                         itemViewModel: itemViewModel,
                                         shouldAllowReply: shouldAllowReply)
        case .quotedReply:
            delegate.didLongPressQuote(cell,
                                       itemViewModel: itemViewModel,
                                       shouldAllowReply: shouldAllowReply)
        case .systemMessage:
            delegate.didLongPressSystemMessage(cell, itemViewModel: itemViewModel)
        case .paymentMessage:
            delegate.didLongPressPaymentMessage(cell, itemViewModel: itemViewModel, shouldAllowReply: shouldAllowReply)
        case .bodyText:
            break
        }
    }

    func startGesture(cell: CVCell) {
        guard let delegate = self.delegate else {
            owsFailDebug("Missing delegate.")
            return
        }

        switch gestureLocation {
        case .bodyText(let item):
            delegate.didLongPressBodyTextItem(item)
        default:
            // Case will be handled by context menu gesture recognizer
            break
        }
    }

    func handleLongPress(_ sender: UILongPressGestureRecognizer) {
        guard let delegate = self.delegate else {
            owsFailDebug("Missing delegate.")
            return
        }

        switch sender.state {
        case .began:
            // We use startGesture(cell:) to start handling the gesture.
            owsFailDebug("Unexpected state.")
        case .changed:
            delegate.didChangeLongPress(itemViewModel)
        case .ended:
            delegate.didEndLongPress(itemViewModel)
        case .failed, .cancelled:
            delegate.didCancelLongPress(itemViewModel)
        case .possible:
            owsFailDebug("Unexpected state.")
        @unknown default:
            owsFailDebug("Invalid state.")
        }
    }
}

// MARK: -

public class CVPanHandler {
    public enum PanType {
        case messageSwipeAction
        case scrubAudio
    }
    public let panType: PanType

    private weak var delegate: CVComponentDelegate?
    private let renderItem: CVRenderItem

    public var interactionId: String { renderItem.interactionUniqueId }

    // If the gesture ended now, would we perform a reply?
    public enum ActiveDirection {
        case left
        case right
        case none
    }
    public var activeDirection: ActiveDirection = .none
    var messageDetailViewController: MessageDetailViewController?

    public var percentDrivenTransition: UIPercentDrivenInteractiveTransition?

    init(delegate: CVComponentDelegate, panType: PanType, renderItem: CVRenderItem) {
        self.delegate = delegate
        self.panType = panType
        self.renderItem = renderItem
    }

    func startGesture(sender: UIPanGestureRecognizer,
                      cell: CVCell,
                      messageSwipeActionState: CVMessageSwipeActionState) {
        guard let delegate = self.delegate else {
            owsFailDebug("Missing delegate.")
            return
        }

        // When the gesture starts, the "reference" of the initial
        // view positions should already be set, but the progress
        // should not yet be set.
        owsAssertDebug(messageSwipeActionState.getProgress(interactionId: interactionId) == nil)

        cell.startPanGesture(sender: sender,
                             panHandler: self,
                             componentDelegate: delegate,
                             messageSwipeActionState: messageSwipeActionState)

        if panType == .messageSwipeAction {
            owsAssertDebug(messageSwipeActionState.getProgress(interactionId: interactionId) != nil)
        }
    }

    func handlePan(sender: UIPanGestureRecognizer,
                   cell: CVCell,
                   messageSwipeActionState: CVMessageSwipeActionState) {
        guard let delegate = self.delegate else {
            owsFailDebug("Missing delegate.")
            return
        }

        if panType == .messageSwipeAction {
            owsAssertDebug(messageSwipeActionState.getProgress(interactionId: interactionId) != nil)
        }
        cell.handlePanGesture(sender: sender,
                              panHandler: self,
                              componentDelegate: delegate,
                              messageSwipeActionState: messageSwipeActionState)
    }
}
