// MIT License
//
// Copyright (c) 2017-2022 MessageKit
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

import Combine
import Foundation
import InputBarAccessoryView
import UIKit

extension MessagesViewController {
  // MARK: Internal

  // MARK: - Register Observers

  internal func addKeyboardObservers() {
    keyboardManager.bind(
      inputAccessoryView: inputContainerView, 
      withAdditionalBottomSpace: { [weak self] in self?.inputBarAdditionalBottomSpace() ?? 0 }
    )
    keyboardManager.bind(to: messagesCollectionView)

    /// Observe didBeginEditing to scroll content to last item if necessary
    NotificationCenter.default
      .publisher(for: UITextView.textDidBeginEditingNotification)
      .subscribe(on: DispatchQueue.global())
      /// Wait for inputBar frame change animation to end
      .delay(for: .milliseconds(200), scheduler: DispatchQueue.main)
      .receive(on: DispatchQueue.main)
      .sink { [weak self] notification in
        self?.handleTextViewDidBeginEditing(notification)
      }
      .store(in: &disposeBag)

    NotificationCenter.default
      .publisher(for: UITextView.textDidChangeNotification)
      .subscribe(on: DispatchQueue.global())
      .receive(on: DispatchQueue.main)
      .compactMap { $0.object as? InputTextView }
      .filter { [weak self] textView in
        textView == self?.messageInputBar.inputTextView
      }
      .map(\.text)
      .removeDuplicates()
      .delay(for: .milliseconds(50), scheduler: DispatchQueue.main) /// Wait for next runloop to lay out inputView properly
      .sink { [weak self] _ in
        self?.updateMessageCollectionViewBottomInset()

        if !(self?.maintainPositionOnInputBarHeightChanged ?? false) {
          self?.messagesCollectionView.scrollToLastItem()
        }
      }
      .store(in: &disposeBag)

    NotificationCenter.default
      .publisher(for: UITextInputMode.currentInputModeDidChangeNotification)
      .subscribe(on: DispatchQueue.global())
      .receive(on: DispatchQueue.main)
      .removeDuplicates()
      .delay(for: .milliseconds(50), scheduler: DispatchQueue.main) /// Wait for next runloop to lay out inputView properly
      .sink { [weak self] _ in
          self?.updateMessageCollectionViewBottomInset()
          
          if !(self?.maintainPositionOnInputBarHeightChanged ?? false) {
              self?.messagesCollectionView.scrollToLastItem()
          }
      }
      .store(in: &disposeBag)

    /// Observe frame change of the input bar container to update collectioView bottom inset
    inputContainerView.publisher(for: \.center)
      .receive(on: DispatchQueue.main)
      .removeDuplicates()
      .sink(receiveValue: { [weak self] _ in
        self?.updateMessageCollectionViewBottomInset()
      })
      .store(in: &disposeBag)
  }

  // MARK: - Updating insets

  /// Updates bottom messagesCollectionView inset based on the position of inputContainerView
  internal func updateMessageCollectionViewBottomInset() {
    let collectionViewHeight = messagesCollectionView.frame.maxY
    let newBottomInset = collectionViewHeight - (inputContainerView.frame.minY - additionalBottomInset) -
      automaticallyAddedBottomInset
    let normalizedNewBottomInset = max(0, newBottomInset)
    let differenceOfBottomInset = newBottomInset - messageCollectionViewBottomInset

    UIView.performWithoutAnimation {
      guard differenceOfBottomInset != 0 else { return }
      messagesCollectionView.contentInset.bottom = normalizedNewBottomInset
      messagesCollectionView.verticalScrollIndicatorInsets.bottom = newBottomInset
    }
  }

  // MARK: Private

  /// UIScrollView can automatically add safe area insets to its contentInset,
  /// which needs to be accounted for when setting the contentInset based on screen coordinates.
  ///
  /// - Returns: The distance automatically added to contentInset.bottom, if any.
  private var automaticallyAddedBottomInset: CGFloat {
    messagesCollectionView.adjustedContentInset.bottom - messageCollectionViewBottomInset
  }

  private var messageCollectionViewBottomInset: CGFloat {
    messagesCollectionView.contentInset.bottom
  }

  /// UIScrollView can automatically add safe area insets to its contentInset,
  /// which needs to be accounted for when setting the contentInset based on screen coordinates.
  ///
  /// - Returns: The distance automatically added to contentInset.top, if any.
  private var automaticallyAddedTopInset: CGFloat {
    messagesCollectionView.adjustedContentInset.top - messageCollectionViewTopInset
  }

  private var messageCollectionViewTopInset: CGFloat {
    messagesCollectionView.contentInset.top
  }

  // MARK: - Private methods

  private func handleTextViewDidBeginEditing(_ notification: Notification) {
    guard
      scrollsToLastItemOnKeyboardBeginsEditing,
      let inputTextView = notification.object as? InputTextView,
      inputTextView === messageInputBar.inputTextView
    else {
      return
    }
    messagesCollectionView.scrollToLastItem()
  }
}
