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

import AVFoundation
import Photos
import SignalServiceKit
import UIKit

protocol VideoEditorViewDelegate: AnyObject {
    func videoEditorViewPlaybackTimeDidChange(_ videoEditorView: VideoEditorView)
}

protocol VideoEditorViewControllerProviding: AnyObject {
    func viewController(forVideoEditorView videoEditorView: VideoEditorView) -> UIViewController
}

// A view for editing outgoing video attachments.
class VideoEditorView: UIView {

    weak var delegate: VideoEditorViewDelegate?
    weak var dataSource: VideoEditorDataSource?
    weak var viewControllerProvider: VideoEditorViewControllerProviding?

    private let model: VideoEditorModel

    var isTrimmingVideo: Bool = false

    private lazy var playerView: VideoPlayerView = {
        let playerView = VideoPlayerView()
        playerView.videoPlayer = VideoPlayer(decryptedFileUrl: URL(fileURLWithPath: model.srcVideoPath))
        playerView.delegate = self
        return playerView
    }()
    private lazy var playButton: UIButton = {
        let playButton = RoundMediaButton(image: UIImage(imageLiteralResourceName: "play-fill-32"), backgroundStyle: .blur)
        playButton.accessibilityLabel = OWSLocalizedString("PLAY_BUTTON_ACCESSABILITY_LABEL",
                                                           comment: "Accessibility label for button to start media playback")
        // this makes the blur circle 72 pts in diameter
        playButton.contentEdgeInsets = UIEdgeInsets(margin: 26)
        // play button must be slightly off-center to appear centered
        playButton.imageEdgeInsets = UIEdgeInsets(top: 0, leading: 3, bottom: 0, trailing: -3)
        playButton.addTarget(self, action: #selector(playButtonTapped), for: .touchUpInside)
        return playButton
    }()

    init(model: VideoEditorModel,
         delegate: VideoEditorViewDelegate,
         dataSource: VideoEditorDataSource,
         viewControllerProvider: VideoEditorViewControllerProviding) {

        self.model = model
        self.delegate = delegate
        self.dataSource = dataSource
        self.viewControllerProvider = viewControllerProvider

        super.init(frame: .zero)

        backgroundColor = .black
    }

    @available(*, unavailable, message: "use other init() instead.")
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    // MARK: - Views

    func configureSubviews() {
        let aspectRatio: CGFloat = model.displaySize.width / model.displaySize.height
        addSubviewWithScaleAspectFitLayout(view: playerView, aspectRatio: aspectRatio)
        playerView.setContentHuggingLow()
        playerView.setCompressionResistanceLow()
        playerView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTapPlayerView(_:))))

        addSubview(playButton)
        playButton.autoAlignAxis(.horizontal, toSameAxisOf: playerView)
        playButton.autoAlignAxis(.vertical, toSameAxisOf: playerView)

        ensureSeekReflectsTrimming()
    }

    private func addSubviewWithScaleAspectFitLayout(view: UIView, aspectRatio: CGFloat) {
        addSubview(view)
        // This emulates the behavior of contentMode = .scaleAspectFit using iOS auto layout constraints.
        addConstraints({
            let constraints = [ view.centerXAnchor.constraint(equalTo: centerXAnchor),
                                view.centerYAnchor.constraint(equalTo: centerYAnchor) ]
            constraints.forEach { $0.priority = .defaultHigh - 100 }
            return constraints
        }())
        addConstraint(view.topAnchor.constraint(greaterThanOrEqualTo: topAnchor))
        view.autoPin(toAspectRatio: aspectRatio)
        view.autoMatch(.width, to: .width, of: self, withMultiplier: 1.0, relation: .lessThanOrEqual)
        view.autoMatch(.height, to: .height, of: self, withMultiplier: 1.0, relation: .lessThanOrEqual)
        NSLayoutConstraint.autoSetPriority(.defaultHigh) {
            view.autoMatch(.width, to: .width, of: self, withMultiplier: 1.0, relation: .equal)
            view.autoMatch(.height, to: .height, of: self, withMultiplier: 1.0, relation: .equal)
        }
    }

    // MARK: - Event Handlers

    @objc
    private func didTapPlayerView(_ gestureRecognizer: UIGestureRecognizer) {
        togglePlayback()
    }

    @objc
    private func playButtonTapped() {
        togglePlayback()
    }

    private func togglePlayback() {
        if isPlaying {
            pauseVideo()
        } else {
            playVideo()
        }
    }

    // MARK: - Video

    var trimmedStartSeconds: TimeInterval {
        return model.trimmedStartSeconds
    }

    var trimmedEndSeconds: TimeInterval {
        return model.trimmedEndSeconds
    }

    @discardableResult
    func pauseIfPlaying() -> Bool {
        guard playerView.isPlaying else {
            return false
        }
        playerView.pause()
        return true
    }

    func seek(toSeconds seconds: TimeInterval) {
        playerView.seek(to: CMTime(seconds: seconds, preferredTimescale: model.untrimmedDuration.timescale))
    }

    func playVideo() {
        if ensureSeekReflectsTrimming() {
            // If this delay isn't induced VideoPlayer.play() would reset
            // current position to 0, likely because AVPlayer hasn't yet
            // had a chance to update its currentTime.
            DispatchQueue.main.async {
                self.playerView.play()
            }
        } else {
            playerView.play()
        }
    }

    @discardableResult
    func ensureSeekReflectsTrimming() -> Bool {
        var shouldSeekToStart = false
        if currentTimeSeconds < trimmedStartSeconds {
            // If playback cursor is before the start of the clipping,
            // restart playback.
            shouldSeekToStart = true
        } else {
            // If playback cursor is very near the end of the clipping,
            // restart playback.
            let toleranceSeconds: TimeInterval = 0.1
            if currentTimeSeconds > trimmedEndSeconds - toleranceSeconds {
                shouldSeekToStart = true
            }
        }

        if shouldSeekToStart {
            seek(toSeconds: trimmedStartSeconds)
        }
        return shouldSeekToStart
    }

    private func pauseVideo() {
        playerView.pause()
    }

    private var isShowingPlayButton = true

    private func updateControls() {
        AssertIsOnMainThread()

        if isPlaying {
            if isShowingPlayButton {
                isShowingPlayButton = false
                UIView.animate(withDuration: 0.1) {
                    self.playButton.alpha = 0.0
                }
            }
        } else {
            if !isShowingPlayButton {
                isShowingPlayButton = true
                UIView.animate(withDuration: 0.1) {
                    self.playButton.alpha = 1.0
                }
            }
        }
    }

    // MARK: - Actions

    @objc
    private func didTapSave(sender: UIButton) {
        playerView.pause()

        guard let viewControllerProvider = viewControllerProvider else {
            owsFailDebug("Missing viewControllerProvider.")
            return
        }
        let viewController = viewControllerProvider.viewController(forVideoEditorView: self)
        viewController.ows_askForMediaLibraryPermissions { isGranted in
            AssertIsOnMainThread()

            guard isGranted else { return }

            ModalActivityIndicatorViewController.present(fromViewController: viewController, canCancel: false) { modalVC in
                firstly {
                    self.saveVideoPromise()
                }.done(on: DispatchQueue.main) {
                    modalVC.dismiss()
                }.catch { _ in
                    modalVC.dismiss {
                        OWSActionSheets.showErrorAlert(message: OWSLocalizedString("ERROR_COULD_NOT_SAVE_VIDEO", comment: "Error indicating that 'save video' failed."))
                    }
                }
            }
        }
    }

    private func saveVideoPromise() -> Promise<Void> {
        // Creates a copy of a file in a new temporary path
        // The file path returned in a Result is guaranteed valid for the Result's lifetime
        // Making a copy protects us from any modifications to a file we don't own
        func createCopyOfFile(_ path: String) throws -> String {
            guard let fileExtension = path.fileExtension else {
                throw OWSAssertionError("Missing fileExtension.")
            }
            let dstPath = OWSFileSystem.temporaryFilePath(fileExtension: fileExtension)
            try FileManager.default.copyItem(atPath: path, toPath: dstPath)
            return dstPath
        }

        return firstly(on: DispatchQueue.global()) { () -> Promise<String> in
            guard self.model.needsRender else {
                // Nothing to render, just use the original file
                let copy = try createCopyOfFile(self.model.srcVideoPath)
                return Promise.value(copy)
            }

            return self.model.ensureCurrentRender().result.map(on: DispatchQueue.global()) { result in
                try createCopyOfFile(result.getResultPath())
            }

        }.then(on: DispatchQueue.global()) { (videoFilePath: String) -> Promise<Void> in
            Promise { future in
                let videoUrl = URL(fileURLWithPath: videoFilePath)

                PHPhotoLibrary.shared().performChanges {
                    PHAssetCreationRequest.creationRequestForAssetFromVideo(atFileURL: videoUrl)
                } completionHandler: { (didSucceed, error) in
                    OWSFileSystem.deleteFileIfExists(videoFilePath)

                    if let error = error {
                        future.reject(error)
                        return
                    }
                    guard didSucceed else {
                        future.reject(OWSAssertionError("Video export failed."))
                        return
                    }
                    future.resolve()
                }
            }
        }
    }
}

extension VideoEditorView: VideoPlaybackState {

    var isPlaying: Bool { playerView.isPlaying }

    var currentTimeSeconds: TimeInterval { playerView.currentTimeSeconds }
}

extension VideoEditorView: VideoPlayerViewDelegate {

    func videoPlayerViewStatusDidChange(_ view: VideoPlayerView) {
        updateControls()
    }

    func videoPlayerViewPlaybackTimeDidChange(_ view: VideoPlayerView) {
        // Trimming the video also changes current playback position
        // and we don't need the code below to be executed when that happens.
        guard !isTrimmingVideo else {
            return
        }

        // Prevent playback past the end of the trimming.
        guard currentTimeSeconds <= trimmedEndSeconds else {
            playerView.stop()
            return
        }

        delegate?.videoEditorViewPlaybackTimeDidChange(self)
    }
}
