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

import Foundation
import AVFoundation
import PromiseKit

protocol PhotoCaptureViewControllerDelegate: AnyObject {
    func photoCaptureViewController(_ photoCaptureViewController: PhotoCaptureViewController, didFinishProcessingAttachment attachment: SignalAttachment)
    func photoCaptureViewControllerDidCancel(_ photoCaptureViewController: PhotoCaptureViewController)
}

enum PhotoCaptureError: Error {
    case assertionError(description: String)
    case initializationFailed
    case captureFailed
}

extension PhotoCaptureError: LocalizedError {
    var localizedDescription: String {
        switch self {
        case .initializationFailed:
            return NSLocalizedString("PHOTO_CAPTURE_UNABLE_TO_INITIALIZE_CAMERA", comment: "alert title")
        case .captureFailed:
            return NSLocalizedString("PHOTO_CAPTURE_UNABLE_TO_CAPTURE_IMAGE", comment: "alert title")
        case .assertionError:
            return NSLocalizedString("PHOTO_CAPTURE_GENERIC_ERROR", comment: "alert title, generic error preventing user from capturing a photo")
        }
    }
}

class PhotoCaptureViewController: OWSViewController {

    weak var delegate: PhotoCaptureViewControllerDelegate?

    private var photoCapture: PhotoCapture!

    deinit {
        UIDevice.current.endGeneratingDeviceOrientationNotifications()
        if let photoCapture = photoCapture {
            photoCapture.stopCapture().done {
                Logger.debug("stopCapture completed")
            }.retainUntilComplete()
        }
    }

    // MARK: - Overrides

    override func loadView() {
        self.view = UIView()
        self.view.backgroundColor = .black
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        setupPhotoCapture()
        setupOrientationMonitoring()
        
        updateNavigationItems()
        updateFlashModeControl()

        let initialCaptureOrientation = AVCaptureVideoOrientation(deviceOrientation: UIDevice.current.orientation) ?? .portrait
        updateIconOrientations(isAnimated: false, captureOrientation: initialCaptureOrientation)

        view.addGestureRecognizer(pinchZoomGesture)
        view.addGestureRecognizer(focusGesture)
        view.addGestureRecognizer(doubleTapToSwitchCameraGesture)
    }

    override var prefersStatusBarHidden: Bool {
        return true
    }

    // MARK: -
    var isRecordingMovie: Bool = false
    let recordingTimerView = RecordingTimerView()

    func updateNavigationItems() {
        if isRecordingMovie {
            navigationItem.leftBarButtonItem = nil
            navigationItem.rightBarButtonItems = nil
            navigationItem.titleView = recordingTimerView
            recordingTimerView.sizeToFit()
        } else {
            navigationItem.titleView = nil
            navigationItem.leftBarButtonItem = dismissControl.barButtonItem
            let fixedSpace = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil)
            fixedSpace.width = 16

            navigationItem.rightBarButtonItems = [switchCameraControl.barButtonItem, fixedSpace, flashModeControl.barButtonItem]
        }
    }

    // HACK: Though we don't have an input accessory view, the VC we are presented above (ConversationVC) does.
    // If the app is backgrounded and then foregrounded, when OWSWindowManager calls mainWindow.makeKeyAndVisible
    // the ConversationVC's inputAccessoryView will appear *above* us unless we'd previously become first responder.
    override public var canBecomeFirstResponder: Bool {
        Logger.debug("")
        return true
    }

    override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        return .portrait
    }

    // MARK: - Views

    let captureButton = CaptureButton()
    var previewView: CapturePreviewView!

    class PhotoControl {
        let button: OWSButton
        let barButtonItem: UIBarButtonItem

        init(imageName: String, block: @escaping () -> Void) {
            self.button = OWSButton(imageName: imageName, tintColor: .ows_white, block: block)
            if #available(iOS 10, *) {
                button.autoPinToSquareAspectRatio()
            } else {
                button.sizeToFit()
            }

            button.layer.shadowOffset = CGSize.zero
            button.layer.shadowOpacity = 0.35
            button.layer.shadowRadius = 4

            self.barButtonItem = UIBarButtonItem(customView: button)
        }

        func setImage(imageName: String) {
            button.setImage(imageName: imageName)
        }
    }

    private lazy var dismissControl: PhotoControl = {
        return PhotoControl(imageName: "X") { [weak self] in
            self?.didTapClose()
        }
    }()

    private lazy var switchCameraControl: PhotoControl = {
        return PhotoControl(imageName: "ic_switch_camera") { [weak self] in
            self?.didTapSwitchCamera()
        }
    }()

    private lazy var flashModeControl: PhotoControl = {
        return PhotoControl(imageName: "ic_flash_mode_auto") { [weak self] in
            self?.didTapFlashMode()
        }
    }()

    lazy var pinchZoomGesture: UIPinchGestureRecognizer = {
        return UIPinchGestureRecognizer(target: self, action: #selector(didPinchZoom(pinchGesture:)))
    }()

    lazy var focusGesture: UITapGestureRecognizer = {
        return UITapGestureRecognizer(target: self, action: #selector(didTapFocusExpose(tapGesture:)))
    }()

    lazy var doubleTapToSwitchCameraGesture: UITapGestureRecognizer = {
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didDoubleTapToSwitchCamera(tapGesture:)))
        tapGesture.numberOfTapsRequired = 2
        return tapGesture
    }()

    // MARK: - Events

    @objc
    func didTapClose() {
        self.delegate?.photoCaptureViewControllerDidCancel(self)
    }

    @objc
    func didTapSwitchCamera() {
        Logger.debug("")
        switchCamera()
    }

    @objc
    func didDoubleTapToSwitchCamera(tapGesture: UITapGestureRecognizer) {
        Logger.debug("")
        switchCamera()
    }

    private func switchCamera() {
        UIView.animate(withDuration: 0.2) {
            let epsilonToForceCounterClockwiseRotation: CGFloat = 0.00001
            self.switchCameraControl.button.transform = self.switchCameraControl.button.transform.rotate(.pi + epsilonToForceCounterClockwiseRotation)
        }
        photoCapture.switchCamera().catch { error in
            self.showFailureUI(error: error)
        }.retainUntilComplete()
    }

    @objc
    func didTapFlashMode() {
        Logger.debug("")
        photoCapture.switchFlashMode().done {
            self.updateFlashModeControl()
        }.retainUntilComplete()
    }

    @objc
    func didPinchZoom(pinchGesture: UIPinchGestureRecognizer) {
        switch pinchGesture.state {
        case .began: fallthrough
        case .changed:
            photoCapture.updateZoom(scaleFromPreviousZoomFactor: pinchGesture.scale)
        case .ended:
            photoCapture.completeZoom(scaleFromPreviousZoomFactor: pinchGesture.scale)
        default:
            break
        }
    }

    @objc
    func didTapFocusExpose(tapGesture: UITapGestureRecognizer) {
        let viewLocation = tapGesture.location(in: view)
        let devicePoint = previewView.previewLayer.captureDevicePointConverted(fromLayerPoint: viewLocation)
        photoCapture.focus(with: .autoFocus, exposureMode: .autoExpose, at: devicePoint, monitorSubjectAreaChange: true)
    }

    // MARK: - Orientation

    private func setupOrientationMonitoring() {
        UIDevice.current.beginGeneratingDeviceOrientationNotifications()

        NotificationCenter.default.addObserver(self,
                                               selector: #selector(didChangeDeviceOrientation),
                                               name: UIDevice.orientationDidChangeNotification,
                                               object: UIDevice.current)
    }

    var lastKnownCaptureOrientation: AVCaptureVideoOrientation = .portrait

    @objc
    func didChangeDeviceOrientation(notification: Notification) {
        let currentOrientation = UIDevice.current.orientation

        if let captureOrientation = AVCaptureVideoOrientation(deviceOrientation: currentOrientation) {
            // since the "face up" and "face down" orientations aren't reflected in the photo output,
            // we need to capture the last known _other_ orientation so we can reflect the appropriate
            // portrait/landscape in our captured photos.
            Logger.verbose("lastKnownCaptureOrientation: \(lastKnownCaptureOrientation)->\(captureOrientation)")
            lastKnownCaptureOrientation = captureOrientation
            updateIconOrientations(isAnimated: true, captureOrientation: captureOrientation)
        }
    }

    // MARK: -

    private func updateIconOrientations(isAnimated: Bool, captureOrientation: AVCaptureVideoOrientation) {
        Logger.verbose("captureOrientation: \(captureOrientation)")

        let transformFromOrientation: CGAffineTransform
        switch captureOrientation {
        case .portrait:
            transformFromOrientation = .identity
        case .portraitUpsideDown:
            transformFromOrientation = CGAffineTransform(rotationAngle: .pi)
        case .landscapeLeft:
            transformFromOrientation = CGAffineTransform(rotationAngle: .halfPi)
        case .landscapeRight:
            transformFromOrientation = CGAffineTransform(rotationAngle: -1 * .halfPi)
        }

        // Don't "unrotate" the switch camera icon if the front facing camera had been selected.
        let tranformFromCameraType: CGAffineTransform = photoCapture.desiredPosition == .front ? CGAffineTransform(rotationAngle: -.pi) : .identity

        let updateOrientation = {
            self.flashModeControl.button.transform = transformFromOrientation
            self.switchCameraControl.button.transform   = transformFromOrientation.concatenating(tranformFromCameraType)
        }

        if isAnimated {
            UIView.animate(withDuration: 0.3, animations: updateOrientation)
        } else {
            updateOrientation()
        }
    }

    private func setupPhotoCapture() {
        photoCapture = PhotoCapture()
        photoCapture.delegate = self
        captureButton.delegate = photoCapture
        previewView = CapturePreviewView(session: photoCapture.session)

        photoCapture.startCapture().done { [weak self] in
            guard let self = self else { return }

            self.showCaptureUI()
        }.catch { [weak self] error in
            guard let self = self else { return }

            self.showFailureUI(error: error)
        }.retainUntilComplete()
    }

    private func showCaptureUI() {
        Logger.debug("")
        view.addSubview(previewView)
        if UIDevice.current.hasIPhoneXNotch {
            previewView.autoPinEdgesToSuperviewEdges()
        } else {
            previewView.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets(top: 0, leading: 0, bottom: 40, trailing: 0))
        }

        view.addSubview(captureButton)
        captureButton.autoHCenterInSuperview()
        captureButton.centerYAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor, constant: SendMediaNavigationController.bottomButtonsCenterOffset).isActive = true
    }

    private func showFailureUI(error: Error) {
        Logger.error("error: \(error)")

        OWSAlerts.showAlert(title: nil,
                            message: error.localizedDescription,
                            buttonTitle: CommonStrings.dismissButton,
                            buttonAction: { [weak self] _ in self?.dismiss(animated: true) })
    }

    private func updateFlashModeControl() {
        let imageName: String
        switch photoCapture.flashMode {
        case .auto:
            imageName = "ic_flash_mode_auto"
        case .on:
            imageName = "ic_flash_mode_on"
        case .off:
            imageName = "ic_flash_mode_off"
        default: preconditionFailure()
        }

        self.flashModeControl.setImage(imageName: imageName)
    }
}

extension PhotoCaptureViewController: PhotoCaptureDelegate {

    // MARK: - Photo

    func photoCapture(_ photoCapture: PhotoCapture, didFinishProcessingAttachment attachment: SignalAttachment) {
        delegate?.photoCaptureViewController(self, didFinishProcessingAttachment: attachment)
    }

    func photoCapture(_ photoCapture: PhotoCapture, processingDidError error: Error) {
        showFailureUI(error: error)
    }

    // MARK: - Video

    func photoCaptureDidBeginVideo(_ photoCapture: PhotoCapture) {
        isRecordingMovie = true
        updateNavigationItems()
        recordingTimerView.startCounting()
    }

    func photoCaptureDidCompleteVideo(_ photoCapture: PhotoCapture) {
        isRecordingMovie = false
        recordingTimerView.stopCounting()
        updateNavigationItems()
    }

    func photoCaptureDidCancelVideo(_ photoCapture: PhotoCapture) {
        owsFailDebug("If we ever allow this, we should test.")
        isRecordingMovie = false
        recordingTimerView.stopCounting()
        updateNavigationItems()
    }

    // MARK: -

    var zoomScaleReferenceHeight: CGFloat? {
        return view.bounds.height
    }

    var captureOrientation: AVCaptureVideoOrientation {
        return lastKnownCaptureOrientation
    }
}

// MARK: - Views

protocol CaptureButtonDelegate: AnyObject {
    // MARK: Photo
    func didTapCaptureButton(_ captureButton: CaptureButton)

    // MARK: Video
    func didBeginLongPressCaptureButton(_ captureButton: CaptureButton)
    func didCompleteLongPressCaptureButton(_ captureButton: CaptureButton)
    func didCancelLongPressCaptureButton(_ captureButton: CaptureButton)

    var zoomScaleReferenceHeight: CGFloat? { get }
    func longPressCaptureButton(_ captureButton: CaptureButton, didUpdateZoomAlpha zoomAlpha: CGFloat)
}

class CaptureButton: UIView {

    let innerButton = CircleView()

    var tapGesture: UITapGestureRecognizer!

    var longPressGesture: UILongPressGestureRecognizer!
    let longPressDuration = 0.5

    let zoomIndicator = CircleView()

    weak var delegate: CaptureButtonDelegate?

    let defaultDiameter: CGFloat = ScaleFromIPhone5To7Plus(60, 80)
    let recordingDiameter: CGFloat = ScaleFromIPhone5To7Plus(68, 120)
    var innerButtonSizeConstraints: [NSLayoutConstraint]!
    var zoomIndicatorSizeConstraints: [NSLayoutConstraint]!

    override init(frame: CGRect) {
        super.init(frame: frame)

        tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTap))
        innerButton.addGestureRecognizer(tapGesture)

        longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(didLongPress))
        longPressGesture.minimumPressDuration = longPressDuration
        innerButton.addGestureRecognizer(longPressGesture)

        addSubview(innerButton)
        innerButtonSizeConstraints = autoSetDimensions(to: CGSize(width: defaultDiameter, height: defaultDiameter))
        innerButton.backgroundColor = UIColor.ows_white.withAlphaComponent(0.33)
        innerButton.layer.shadowOffset = .zero
        innerButton.layer.shadowOpacity = 0.33
        innerButton.layer.shadowRadius = 2
        innerButton.autoPinEdgesToSuperviewEdges()

        addSubview(zoomIndicator)
        zoomIndicatorSizeConstraints = zoomIndicator.autoSetDimensions(to: CGSize(width: defaultDiameter, height: defaultDiameter))
        zoomIndicator.isUserInteractionEnabled = false
        zoomIndicator.layer.borderColor = UIColor.ows_white.cgColor
        zoomIndicator.layer.borderWidth = 1.5
        zoomIndicator.autoAlignAxis(.horizontal, toSameAxisOf: innerButton)
        zoomIndicator.autoAlignAxis(.vertical, toSameAxisOf: innerButton)
    }

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

    // MARK: - Gestures

    @objc
    func didTap(_ gesture: UITapGestureRecognizer) {
        delegate?.didTapCaptureButton(self)
    }

    var initialTouchLocation: CGPoint?

    @objc
    func didLongPress(_ gesture: UILongPressGestureRecognizer) {
        Logger.verbose("")

        guard let gestureView = gesture.view else {
            owsFailDebug("gestureView was unexpectedly nil")
            return
        }

        switch gesture.state {
        case .possible: break
        case .began:
            initialTouchLocation = gesture.location(in: gesture.view)
            delegate?.didBeginLongPressCaptureButton(self)
            UIView.animate(withDuration: 0.2) {
                self.innerButtonSizeConstraints.forEach { $0.constant = self.recordingDiameter }
                self.zoomIndicatorSizeConstraints.forEach { $0.constant = self.recordingDiameter }
                self.superview?.layoutIfNeeded()
            }
        case .changed:
            guard let referenceHeight = delegate?.zoomScaleReferenceHeight else {
                owsFailDebug("referenceHeight was unexpectedly nil")
                return
            }

            guard referenceHeight > 0 else {
                owsFailDebug("referenceHeight was unexpectedly <= 0")
                return
            }

            guard let initialTouchLocation = initialTouchLocation else {
                owsFailDebug("initialTouchLocation was unexpectedly nil")
                return
            }

            let currentLocation = gesture.location(in: gestureView)
            let minDistanceBeforeActivatingZoom: CGFloat = 30
            let distance = initialTouchLocation.y - currentLocation.y - minDistanceBeforeActivatingZoom
            let distanceForFullZoom = referenceHeight / 4
            let ratio = distance / distanceForFullZoom

            let alpha = ratio.clamp(0, 1)

            Logger.verbose("distance: \(distance), alpha: \(alpha)")

            let zoomIndicatorDiameter = CGFloatLerp(recordingDiameter, 3, alpha)
            self.zoomIndicatorSizeConstraints.forEach { $0.constant = zoomIndicatorDiameter }
            zoomIndicator.superview?.layoutIfNeeded()

            delegate?.longPressCaptureButton(self, didUpdateZoomAlpha: alpha)
        case .ended:
            UIView.animate(withDuration: 0.2) {
                self.innerButtonSizeConstraints.forEach { $0.constant = self.defaultDiameter }
                self.zoomIndicatorSizeConstraints.forEach { $0.constant = self.defaultDiameter }

                self.superview?.layoutIfNeeded()
            }
            delegate?.didCompleteLongPressCaptureButton(self)
        case .cancelled, .failed:

            UIView.animate(withDuration: 0.2) {
                self.innerButtonSizeConstraints.forEach { $0.constant = self.defaultDiameter }
                self.zoomIndicatorSizeConstraints.forEach { $0.constant = self.defaultDiameter }

                self.superview?.layoutIfNeeded()
            }
            delegate?.didCancelLongPressCaptureButton(self)
        default: break
        }
    }
}

class CapturePreviewView: UIView {

    let previewLayer: AVCaptureVideoPreviewLayer

    override var bounds: CGRect {
        didSet {
            previewLayer.frame = bounds
        }
    }

    init(session: AVCaptureSession) {
        previewLayer = AVCaptureVideoPreviewLayer(session: session)
        super.init(frame: .zero)
        self.contentMode = .scaleAspectFill
        previewLayer.frame = bounds
        layer.addSublayer(previewLayer)
    }

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

class RecordingTimerView: UIView {

    let stackViewSpacing: CGFloat = 4

    override init(frame: CGRect) {
        super.init(frame: frame)

        let stackView = UIStackView(arrangedSubviews: [icon, label])
        stackView.axis = .horizontal
        stackView.alignment = .center
        stackView.spacing = stackViewSpacing

        addSubview(stackView)
        stackView.autoPinEdgesToSuperviewMargins()

        updateView()
    }

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

    // MARK: - Subviews

    private lazy var label: UILabel = {
        let label = UILabel()
        label.font = UIFont.ows_monospacedDigitFont(withSize: 20)
        label.textAlignment = .center
        label.textColor = UIColor.white
        label.layer.shadowOffset = CGSize.zero
        label.layer.shadowOpacity = 0.35
        label.layer.shadowRadius = 4

        return label
    }()

    static let iconWidth: CGFloat = 6

    private let icon: UIView = {
        let icon = CircleView()
        icon.layer.shadowOffset = CGSize.zero
        icon.layer.shadowOpacity = 0.35
        icon.layer.shadowRadius = 4

        icon.backgroundColor = .red
        icon.autoSetDimensions(to: CGSize(width: iconWidth, height: iconWidth))
        icon.alpha = 0

        return icon
    }()

    // MARK: - Overrides  //

    override func sizeThatFits(_ size: CGSize) -> CGSize {
        if #available(iOS 10, *) {
            return super.sizeThatFits(size)
        } else {
            // iOS9 manual layout sizing required for items in the navigation bar
            var baseSize = label.frame.size
            baseSize.width = baseSize.width + stackViewSpacing + RecordingTimerView.iconWidth + layoutMargins.left + layoutMargins.right
            baseSize.height = baseSize.height + layoutMargins.top + layoutMargins.bottom
            return baseSize
        }
    }

    // MARK: -
    var recordingStartTime: TimeInterval?

    func startCounting() {
        recordingStartTime = CACurrentMediaTime()
        timer = Timer.weakScheduledTimer(withTimeInterval: 0.1, target: self, selector: #selector(updateView), userInfo: nil, repeats: true)
        UIView.animate(withDuration: 0.5,
                       delay: 0,
                       options: [.autoreverse, .repeat],
                       animations: { self.icon.alpha = 1 })
    }

    func stopCounting() {
        timer?.invalidate()
        timer = nil
        icon.layer.removeAllAnimations()
        UIView.animate(withDuration: 0.4) {
            self.icon.alpha = 0
        }
    }

    // MARK: -

    private var timer: Timer?

    private lazy var timeFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "mm:ss"
        formatter.timeZone = TimeZone(identifier: "UTC")!

        return formatter
    }()

    // This method should only be called when the call state is "connected".
    var recordingDuration: TimeInterval {
        guard let recordingStartTime = recordingStartTime else {
            return 0
        }

        return CACurrentMediaTime() - recordingStartTime
    }

    @objc
    private func updateView() {
        let recordingDuration = self.recordingDuration
        Logger.verbose("recordingDuration: \(recordingDuration)")
        let durationDate = Date(timeIntervalSinceReferenceDate: recordingDuration)
        label.text = timeFormatter.string(from: durationDate)
        if #available(iOS 10, *) {
            // do nothing
        } else {
            label.sizeToFit()
        }
    }
}