//
//  Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//

import Foundation
import UIKit
import AVFoundation
import SessionUIKit

protocol AttachmentPrepViewControllerDelegate: AnyObject {
    func prepViewControllerUpdateNavigationBar()

    func prepViewControllerUpdateControls()
}

// MARK: -

public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarDelegate, OWSVideoPlayerDelegate, MediaMessageViewAudioDelegate {
    // We sometimes shrink the attachment view so that it remains somewhat visible
    // when the keyboard is presented.
    public enum AttachmentViewScale {
        case fullsize, compact
    }

    // MARK: - Properties

    weak var prepDelegate: AttachmentPrepViewControllerDelegate?

    let attachmentItem: SignalAttachmentItem
    var attachment: SignalAttachment {
        return attachmentItem.attachment
    }

    private lazy var videoPlayer: OWSVideoPlayer? = {
        guard let videoURL = attachment.dataUrl else {
            owsFailDebug("Missing videoURL")
            return nil
        }

        let player: OWSVideoPlayer = OWSVideoPlayer(url: videoURL)
        player.delegate = self
        
        return player
    }()
    
    // MARK: - UI
    
    fileprivate static let verticalCenterOffset: CGFloat = (
        AttachmentTextToolbar.kMinTextViewHeight + (AttachmentTextToolbar.kToolbarMargin * 2)
    )
    
    private lazy var scrollView: UIScrollView = {
        // Scroll View - used to zoom/pan on images and video
        let scrollView: UIScrollView = UIScrollView()
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        scrollView.delegate = self
        scrollView.showsHorizontalScrollIndicator = false
        scrollView.showsVerticalScrollIndicator = false

        // Panning should stop pretty soon after the user stops scrolling
        scrollView.decelerationRate = UIScrollView.DecelerationRate.fast
        
        return scrollView
    }()
    
    private lazy var contentContainerView: UIView = {
        // Anything that should be shrunk when user pops keyboard lives in the contentContainer.
        let view: UIView = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false

        return view
    }()
    
    private lazy var mediaMessageView: MediaMessageView = {
        let view: MediaMessageView = MediaMessageView(attachment: attachment, mode: .attachmentApproval)
        view.translatesAutoresizingMaskIntoConstraints = false
        view.audioDelegate = self
        view.isHidden = (imageEditorView != nil)
        
        return view
    }()
    
    private lazy var imageEditorView: ImageEditorView? = {
        guard let imageEditorModel = attachmentItem.imageEditorModel else { return nil }
        
        let view: ImageEditorView = ImageEditorView(model: imageEditorModel, delegate: self)
        view.translatesAutoresizingMaskIntoConstraints = false
        
        guard view.configureSubviews() else { return nil }
        
        return view
    }()
    
    private lazy var videoPlayerView: VideoPlayerView? = {
        guard let videoPlayer: OWSVideoPlayer = videoPlayer else { return nil }

        let view: VideoPlayerView = VideoPlayerView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.player = videoPlayer.avPlayer

        let pauseGesture = UITapGestureRecognizer(target: self, action: #selector(didTapPlayerView(_:)))
        view.addGestureRecognizer(pauseGesture)
        
        return view
    }()
    
    private lazy var progressBar: PlayerProgressBar = {
        let progressBar: PlayerProgressBar = PlayerProgressBar()
        progressBar.translatesAutoresizingMaskIntoConstraints = false
        progressBar.player = videoPlayer?.avPlayer
        progressBar.delegate = self
        
        return progressBar
    }()
    
    private lazy var playVideoButton: UIButton = {
        let button: UIButton = UIButton()
        button.translatesAutoresizingMaskIntoConstraints = false
        button.contentMode = .scaleAspectFit
        button.setBackgroundImage(#imageLiteral(resourceName: "CirclePlay"), for: .normal)
        button.addTarget(self, action: #selector(playButtonTapped), for: .touchUpInside)
        
        return button
    }()

    public var shouldHideControls: Bool {
        guard let imageEditorView = imageEditorView else {
            return false
        }
        return imageEditorView.shouldHideControls
    }

    // MARK: - Initializers

    init(attachmentItem: SignalAttachmentItem) {
        self.attachmentItem = attachmentItem
        super.init(nibName: nil, bundle: nil)
        if attachment.hasError {
            owsFailDebug(attachment.error.debugDescription)
        }
    }

    public required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    // MARK: - View Lifecycle
    
    public override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = Colors.navigationBarBackground

        view.addSubview(contentContainerView)
        
        contentContainerView.addSubview(scrollView)
        scrollView.addSubview(mediaMessageView)
        
        if attachment.isImage, let editorView: ImageEditorView = imageEditorView {
            view.addSubview(editorView)
            
            imageEditorUpdateNavigationBar()
        }

        // Hide the play button embedded in the MediaView and replace it with our own.
        // This allows us to zoom in on the media view without zooming in on the button
        // TODO: This for both Audio and Video?
        if attachment.isVideo, let playerView: VideoPlayerView = videoPlayerView {
            mediaMessageView.videoPlayButton.isHidden = true
            mediaMessageView.addSubview(playerView)
            
            // We don't want the progress bar to zoom during "pinch-to-zoom"
            // but we do want it to shrink with the media content when the user
            // pops the keyboard.
            contentContainerView.addSubview(progressBar)
            contentContainerView.addSubview(playVideoButton)
        }
        else if attachment.isAudio, mediaMessageView.audioPlayer != nil {
            contentContainerView.addSubview(progressBar)
        }
        
        setupLayout()
    }

    override public func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        prepDelegate?.prepViewControllerUpdateNavigationBar()
        prepDelegate?.prepViewControllerUpdateControls()
    }

    override public func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        prepDelegate?.prepViewControllerUpdateNavigationBar()
        prepDelegate?.prepViewControllerUpdateControls()
    }
    
    override public func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        
        setupZoomScale()
        ensureAttachmentViewScale(animated: false)
    }
    
    public override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        
        // Note: Need to do this here to ensure it's based on the final sizing
        // otherwise the offsets will be slightly off
        resetContentInset()
    }
    
    // MARK: - Layout
    
    private func setupLayout() {
        NSLayoutConstraint.activate([
            contentContainerView.topAnchor.constraint(equalTo: view.topAnchor),
            contentContainerView.leftAnchor.constraint(equalTo: view.leftAnchor),
            contentContainerView.rightAnchor.constraint(equalTo: view.rightAnchor),
            contentContainerView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            
            scrollView.topAnchor.constraint(equalTo: contentContainerView.topAnchor),
            scrollView.leftAnchor.constraint(equalTo: contentContainerView.leftAnchor),
            scrollView.rightAnchor.constraint(equalTo: contentContainerView.rightAnchor),
            scrollView.bottomAnchor.constraint(equalTo: contentContainerView.bottomAnchor),
            
            mediaMessageView.topAnchor.constraint(equalTo: scrollView.topAnchor),
            mediaMessageView.leftAnchor.constraint(equalTo: scrollView.leftAnchor),
            mediaMessageView.rightAnchor.constraint(equalTo: scrollView.rightAnchor),
            mediaMessageView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
            mediaMessageView.widthAnchor.constraint(equalTo: view.widthAnchor),
            mediaMessageView.heightAnchor.constraint(equalTo: view.heightAnchor)
        ])
        
        if attachment.isImage, let editorView: ImageEditorView = imageEditorView {
            let size: CGSize = (attachment.image()?.size ?? CGSize.zero)
            let isPortrait: Bool = (size.height > size.width)
            
            NSLayoutConstraint.activate([
                editorView.topAnchor.constraint(equalTo: view.topAnchor),
                editorView.leftAnchor.constraint(equalTo: view.leftAnchor),
                editorView.rightAnchor.constraint(equalTo: view.rightAnchor),
                editorView.bottomAnchor.constraint(
                    equalTo: view.bottomAnchor,
                    // Don't offset portrait images as they look fine vertically aligned, horizontal
                    // ones need to be pushed up a bit though
                    constant: (isPortrait ? 0 : -AttachmentPrepViewController.verticalCenterOffset)
                )
            ])
        }
         
        if attachment.isVideo, let playerView: VideoPlayerView = videoPlayerView {
            let playButtonSize: CGFloat = ScaleFromIPhone5(70)
            
            NSLayoutConstraint.activate([
                playerView.topAnchor.constraint(equalTo: mediaMessageView.topAnchor),
                playerView.leftAnchor.constraint(equalTo: mediaMessageView.leftAnchor),
                playerView.rightAnchor.constraint(equalTo: mediaMessageView.rightAnchor),
                playerView.bottomAnchor.constraint(equalTo: mediaMessageView.bottomAnchor),
                
                progressBar.topAnchor.constraint(equalTo: view.topAnchor),
                progressBar.widthAnchor.constraint(equalTo: contentContainerView.widthAnchor),
                progressBar.heightAnchor.constraint(equalToConstant: 44),
                
                playVideoButton.centerXAnchor.constraint(equalTo: contentContainerView.centerXAnchor),
                playVideoButton.centerYAnchor.constraint(
                    equalTo: contentContainerView.centerYAnchor,
                    constant: -AttachmentPrepViewController.verticalCenterOffset
                ),
                playVideoButton.widthAnchor.constraint(equalToConstant: playButtonSize),
                playVideoButton.heightAnchor.constraint(equalToConstant: playButtonSize),
            ])
        }
        else if attachment.isAudio, mediaMessageView.audioPlayer != nil {
            NSLayoutConstraint.activate([
                progressBar.topAnchor.constraint(equalTo: view.topAnchor),
                progressBar.widthAnchor.constraint(equalTo: contentContainerView.widthAnchor),
                progressBar.heightAnchor.constraint(equalToConstant: 44)
            ])
        }
    }

    // MARK: - Navigation Bar

    public func navigationBarItems() -> [UIView] {
        guard let imageEditorView = imageEditorView else {
            return []
        }
        
        return imageEditorView.navigationBarItems()
    }

    // MARK: - Event Handlers

    @objc public func didTapPlayerView(_ gestureRecognizer: UIGestureRecognizer) {
        assert(self.videoPlayer != nil)
        self.pauseVideo()
    }

    @objc public func playButtonTapped() {
        self.playVideo()
    }

    // MARK: - Video

    private func playVideo() {
        guard let videoPlayer = self.videoPlayer else {
            owsFailDebug("video player was unexpectedly nil")
            return
        }

        UIView.animate(withDuration: 0.1) { [weak self] in
            self?.playVideoButton.alpha = 0.0
        }
        
        videoPlayer.play()
    }

    private func pauseVideo() {
        guard let videoPlayer = self.videoPlayer else {
            owsFailDebug("video player was unexpectedly nil")
            return
        }

        videoPlayer.pause()
        
        UIView.animate(withDuration: 0.1) { [weak self] in
            self?.playVideoButton.alpha = 1.0
        }
    }

    public func videoPlayerDidPlayToCompletion(_ videoPlayer: OWSVideoPlayer) {
        UIView.animate(withDuration: 0.1) { [weak self] in
            self?.playVideoButton.alpha = 1.0
        }
    }

    public func playerProgressBarDidStartScrubbing(_ playerProgressBar: PlayerProgressBar) {
        if attachment.isAudio {
            mediaMessageView.pauseAudio()
            return
        }
        
        guard let videoPlayer = self.videoPlayer else {
            owsFailDebug("video player was unexpectedly nil")
            return
        }
        
        videoPlayer.pause()
    }

    public func playerProgressBar(_ playerProgressBar: PlayerProgressBar, scrubbedToTime time: CMTime) {
        if attachment.isAudio {
            mediaMessageView.setAudioTime(currentTime: CMTimeGetSeconds(time))
            progressBar.manuallySetValue(CMTimeGetSeconds(time), durationSeconds: mediaMessageView.audioDurationSeconds)
            return
        }
        
        guard let videoPlayer = self.videoPlayer else {
            owsFailDebug("video player was unexpectedly nil")
            return
        }

        videoPlayer.seek(to: time)
        progressBar.updateState()
    }

    public func playerProgressBar(_ playerProgressBar: PlayerProgressBar, didFinishScrubbingAtTime time: CMTime, shouldResumePlayback: Bool) {
        if attachment.isAudio {
            mediaMessageView.setAudioTime(currentTime: CMTimeGetSeconds(time))
            progressBar.manuallySetValue(CMTimeGetSeconds(time), durationSeconds: mediaMessageView.audioDurationSeconds)
            
            if mediaMessageView.wasPlayingAudio {
                mediaMessageView.playAudio()
            }
            return
        }
        
        guard let videoPlayer = self.videoPlayer else {
            owsFailDebug("video player was unexpectedly nil")
            return
        }

        videoPlayer.seek(to: time)
        progressBar.updateState()
        
        if (shouldResumePlayback) {
            videoPlayer.play()
        }
    }
    
    // MARK: - MediaMessageViewAudioDelegate
    
    public func progressChanged(_ progressSeconds: CGFloat, durationSeconds: CGFloat) {
        progressBar.manuallySetValue(progressSeconds, durationSeconds: durationSeconds)
    }

    // MARK: - Helpers

    var isZoomable: Bool {
        return attachment.isImage || attachment.isVideo
    }

    func zoomOut(animated: Bool) {
        if self.scrollView.zoomScale != self.scrollView.minimumZoomScale {
            self.scrollView.setZoomScale(self.scrollView.minimumZoomScale, animated: animated)
        }
    }

    // When the keyboard is popped, it can obscure the attachment view.
    // so we sometimes allow resizing the attachment.
    var shouldAllowAttachmentViewResizing: Bool = true

    var attachmentViewScale: AttachmentViewScale = .fullsize
    
    public func setAttachmentViewScale(_ attachmentViewScale: AttachmentViewScale, animated: Bool) {
        self.attachmentViewScale = attachmentViewScale
        ensureAttachmentViewScale(animated: animated)
    }

    func ensureAttachmentViewScale(animated: Bool) {
        let animationDuration = animated ? 0.2 : 0
        guard shouldAllowAttachmentViewResizing else {
            if self.contentContainerView.transform != CGAffineTransform.identity {
                UIView.animate(withDuration: animationDuration) {
                    self.contentContainerView.transform = CGAffineTransform.identity
                }
            }
            return
        }

        switch attachmentViewScale {
        case .fullsize:
            guard self.contentContainerView.transform != .identity else {
                return
            }
            UIView.animate(withDuration: animationDuration) {
                self.contentContainerView.transform = CGAffineTransform.identity
            }
        case .compact:
            guard self.contentContainerView.transform == .identity else {
                return
            }
            UIView.animate(withDuration: animationDuration) {
                let kScaleFactor: CGFloat = 0.7
                let scale = CGAffineTransform(scaleX: kScaleFactor, y: kScaleFactor)

                let originalHeight = self.scrollView.bounds.size.height

                // Position the new scaled item to be centered with respect
                // to it's new size.
                let heightDelta = originalHeight * (1 - kScaleFactor)
                let translate = CGAffineTransform(translationX: 0, y: -heightDelta / 2)

                self.contentContainerView.transform = scale.concatenating(translate)
            }
        }
    }
}

// MARK: -

extension AttachmentPrepViewController: UIScrollViewDelegate {

    public func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        if isZoomable {
            return mediaMessageView
        }
        
        // Don't zoom for audio or generic attachments.
        return nil
    }
    
    public func scrollViewDidZoom(_ scrollView: UIScrollView) {
        resetContentInset()
    }

    fileprivate func setupZoomScale() {
        // We only want to setup the zoom scale once (otherwise we get glitchy behaviour
        // when anything forces a re-layout)
        guard abs(scrollView.maximumZoomScale - 1.0) <= CGFloat.leastNormalMagnitude else {
            return
        }
        
        // Ensure bounds have been computed
        guard mediaMessageView.bounds.width > 0, mediaMessageView.bounds.height > 0 else {
            Logger.warn("bad bounds")
            return
        }

        let widthScale: CGFloat = (view.bounds.size.width / mediaMessageView.bounds.width)
        let heightScale: CGFloat = (view.bounds.size.height / mediaMessageView.bounds.height)
        let minScale: CGFloat = min(widthScale, heightScale)

        scrollView.minimumZoomScale = minScale
        scrollView.maximumZoomScale = (minScale * 5)
        scrollView.zoomScale = minScale
    }
    
    // Allow the user to zoom out to 100% of the attachment size if it's smaller
    // than the screen
    fileprivate func resetContentInset() {
        // If the content isn't zoomable then inset the content so it appears centered
        guard isZoomable else {
            scrollView.contentInset = UIEdgeInsets(
                top: -AttachmentPrepViewController.verticalCenterOffset,
                leading: 0,
                bottom: 0,
                trailing: 0
            )
            return
        }
        
        let offsetX: CGFloat = max((scrollView.bounds.width - scrollView.contentSize.width) * 0.5, 0)
        let offsetY: CGFloat = max((scrollView.bounds.height - scrollView.contentSize.height) * 0.5, 0)
        
        scrollView.contentInset = UIEdgeInsets(
            top: offsetY - AttachmentPrepViewController.verticalCenterOffset,
            left: offsetX,
            bottom: 0,
            right: 0
        )
    }
}

// MARK: -

extension AttachmentPrepViewController: ImageEditorViewDelegate {
    public func imageEditor(presentFullScreenView viewController: UIViewController,
                            isTransparent: Bool) {

        let navigationController = OWSNavigationController(rootViewController: viewController)
        navigationController.modalPresentationStyle = (isTransparent
            ? .overFullScreen
            : .fullScreen)
        navigationController.ows_prefersStatusBarHidden = true
        navigationController.view.backgroundColor = Colors.navigationBarBackground

        if let navigationBar = navigationController.navigationBar as? OWSNavigationBar {
            navigationBar.overrideTheme(type: .clear)
        } else {
            owsFailDebug("navigationBar was nil or unexpected class")
        }

        self.present(navigationController, animated: false) {
            // Do nothing.
        }
    }

    public func imageEditorUpdateNavigationBar() {
        prepDelegate?.prepViewControllerUpdateNavigationBar()
    }

    public func imageEditorUpdateControls() {
        prepDelegate?.prepViewControllerUpdateControls()
    }
}