//
//  CameraCaptureView.swift
//  
//  Copyright © 2016-2019 Apple Inc. All rights reserved.
//

import UIKit
import AVFoundation
import Photos
import Vision
import SPCCore

public typealias CameraDevice = AVCaptureDevice.Position

public class CameraCaptureView: UIView {
    private let captureSession = AVCaptureSession()
    private var isCaptureSessionConfigured = false
    private var previewView: VideoPreviewView!
    private var maskImageView: UIImageView?
    private var overlayImageView: UIImageView?
    private var maskLayer: CAShapeLayer?
    private let captureAnimatableLayer = CAShapeLayer()
    private var capturePhotoOutput: AVCapturePhotoOutput?
    private var sessionQueue: DispatchQueue!
    
    private lazy var frontCameraDevice: AVCaptureDevice? = {
        return AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: .front).devices.first
    }()
    
    private lazy var backCameraDevice: AVCaptureDevice? = {
        return AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: .back).devices.first
    }()
    
    // MARK: Public Properties
    
    // The last image captured.
    public var image: UIImage?
    
    // The mask path to be applied to the preview view, and subsequently to a captured image.
    // The mask path will override a mask image, and vice versa.
    public var maskPath: UIBezierPath? {
        get {
            if let cgPath = maskLayer?.path {
                return UIBezierPath(cgPath: cgPath)
            }
            return nil
        }
        set {
            clearMask()
            
            if let newBezierPath = newValue, !newBezierPath.isEmpty {
                let shapeLayer = CAShapeLayer()
                shapeLayer.path = newBezierPath.cgPath
                maskLayer = shapeLayer
                layer.mask = maskLayer
            }
        }
    }
    
    private var cachedMaskImage: UIImage?
    
    // The mask image to be applied to the preview view, and subsequently to a captured image.
    // A mask image will override a mask path, and vice versa.
    // The mask image is resized (aspect-fit) to fill the view.
    public var maskImage: UIImage? {
        get { return maskImageView?.image }
        set {
            clearMask()
            
            if let newImage = newValue, !newImage.isEmpty {
                maskImageView = UIImageView(image: newImage)
                maskImageView?.contentMode = .scaleAspectFit
                mask = maskImageView
            }
        }
    }
    
    // The image to be overlaid onto the preview view, and subsequently onto a captured image.
    // The overlay image is resized (aspect-fit) to fill the view.
    public var overlayImage: UIImage? {
        get { return overlayImageView?.image }
        set {
            if overlayImageView != nil {
                overlayImageView?.removeFromSuperview()
                overlayImageView = nil
            }

            if let newImage = newValue, !newImage.isEmpty {
                let overlayView = UIImageView(image: newImage)
                overlayView.contentMode = .scaleAspectFit
                addSubview(overlayView)
                overlayImageView = overlayView
                setNeedsLayout()
            }
        }
    }
    
    public var overlayImageOffset: CGPoint = CGPoint.zero {
        didSet {
            setNeedsLayout()
        }
    }
    
    public var overlayImageScale: CGFloat = 1.0 {
        didSet {
            setNeedsLayout()
        }
    }
    
    public var isOverlayImageCropped: Bool = true

    public var cameraDevice: CameraDevice = .back {
        didSet {
            self.sessionQueue.async {
                self.updateCameraDevice(self.cameraDevice)
            }
        }
    }
    
    public var zoomFactor: Double {
        get {
            guard let videoCaptureDevice = AVCaptureDevice.default(for: AVMediaType.video) else { return 1.0 }
            return Double(videoCaptureDevice.videoZoomFactor)
        }
        set {
            guard let videoCaptureDevice = AVCaptureDevice.default(for: AVMediaType.video) else { return }
            do {
                var newZoomFactor = max(CGFloat(newValue), 1.0)
                let maxZoomFactor = videoCaptureDevice.activeFormat.videoMaxZoomFactor
                newZoomFactor = min(newZoomFactor, maxZoomFactor)
                try videoCaptureDevice.lockForConfiguration()
                defer { videoCaptureDevice.unlockForConfiguration() }
                videoCaptureDevice.videoZoomFactor = newZoomFactor
            } catch {
                PBLog("\(error)")
            }
        }
    }
    
    public var minZoomFactor: Double {
        guard let videoCaptureDevice = AVCaptureDevice.default(for: AVMediaType.video) else { return 1.0 }
        return Double(videoCaptureDevice.minAvailableVideoZoomFactor)
    }
    
    public var maxZoomFactor: Double {
        guard let videoCaptureDevice = AVCaptureDevice.default(for: AVMediaType.video) else { return 1.0 }
        return Double(videoCaptureDevice.maxAvailableVideoZoomFactor)
    }
    
    public var hasFrontCamera: Bool = {
        let frontDevices = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: .front).devices
        return !frontDevices.isEmpty
    }()
    
    public var isCaptureAnimationEnabled: Bool = true
    
    public var photoTakingFlashColor: UIColor {
        get { return UIColor(cgColor: captureAnimatableLayer.fillColor ?? UIColor.white.cgColor) }
        set { captureAnimatableLayer.fillColor = newValue.cgColor }
    }
    
    public var didTakePhoto: ((_ image: UIImage) -> Void)?
    
    // MARK: Initialization
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        initialize()
    }
    
    required public init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        initialize()
    }
    
    private func initialize() {
        clipsToBounds = true
        previewView = VideoPreviewView()
        previewView.backgroundColor = UIColor.darkGray
        addSubview(previewView)
        
        photoTakingFlashColor = .white
        captureAnimatableLayer.opacity = 0
        captureAnimatableLayer.masksToBounds = true
        layer.addSublayer(captureAnimatableLayer)
 
        sessionQueue = DispatchQueue(label: "session queue")
        previewView.session = self.captureSession
        
        // Register for device orientation changes.
        UIDevice.current.beginGeneratingDeviceOrientationNotifications()
        NotificationCenter.default.addObserver(self, selector: #selector(updateForDeviceOrientation), name: UIDevice.orientationDidChangeNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(onComponentViewSizeChanged), name: .componentViewSizeChanged, object: nil)
        
        // Register for interruption notifications.
        NotificationCenter.default.addObserver(self, selector: #selector(sessionWasInterrupted), name: .AVCaptureSessionWasInterrupted, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(sessionInterruptionEnded), name: .AVCaptureSessionInterruptionEnded, object: nil)
    }
    
    deinit {
        PBLog("CameraCaptureView")
        NotificationCenter.default.removeObserver(self)
    }
    
    // MARK: Layout
    
    override public func layoutSubviews() {
        super.layoutSubviews()
        
        // Fill the frame for now, but this path could be an animated shape such as a capture opening.
        captureAnimatableLayer.path = UIBezierPath(rect: bounds).cgPath
        captureAnimatableLayer.frame = bounds
        
        previewView.frame = bounds
        maskLayer?.frame = bounds
        maskImageView?.frame = bounds
        overlayImageView?.frame = CGRect(x: 0, y: 0, width: bounds.size.width * overlayImageScale, height: bounds.size.height * overlayImageScale)
        overlayImageView?.center = CGPoint(x: bounds.midX + overlayImageOffset.x, y: bounds.midY + overlayImageOffset.y)
    }
    
    // MARK: Private Methods
    
    // Clears the mask path and mask image.
    private func clearMask() {
        mask = nil
        maskImageView = nil
        maskLayer = nil
        layer.mask = nil
        cachedMaskImage = nil
    }
    
    private func configureCaptureSession(completionHandler: ((_ success: Bool) -> Void)) {
        
        var success = false
        
        defer { completionHandler(success) }   // Called on exit
        
        // Get video input for the default camera.
        guard
        let videoCaptureDevice = (cameraDevice == .front) ? frontCameraDevice : backCameraDevice,
            let videoInput = try? AVCaptureDeviceInput(device: videoCaptureDevice)
            else {
                PBLog("Unable to obtain video input for default camera.")
                return
        }
        
        // Create and configure the photo output.
        let photoOutput = AVCapturePhotoOutput()
        photoOutput.isHighResolutionCaptureEnabled = false
        photoOutput.isLivePhotoCaptureEnabled = photoOutput.isLivePhotoCaptureSupported
        
        // Make sure inputs and output can be added to session.
        guard captureSession.canAddInput(videoInput) else { return }
        guard captureSession.canAddOutput(photoOutput) else { return }
        
        // Configure the session.
        captureSession.beginConfiguration()
        captureSession.sessionPreset = AVCaptureSession.Preset.hd1280x720
        captureSession.addInput(videoInput)
        captureSession.addOutput(photoOutput)
        
        captureSession.commitConfiguration()
        
        capturePhotoOutput = photoOutput
        
        isCaptureSessionConfigured = true
        
        success = true
    }
    
    // Camera authorization
    private func checkCameraAuthorization(completionHandler: @escaping ((_ authorized: Bool) -> Void)) {
        
        // Check camera authorization status. Video access is required and audio access is optional.
        // If audio access is denied, audio is not recorded during live photo recording.
        
        switch AVCaptureDevice.authorizationStatus(for: AVMediaType.video) {
            
        case .authorized:
            //The user has previously granted access to the camera.
            completionHandler(true)
            
        case .notDetermined:
            // The user has not yet been presented with the option to grant video access so request access.
            
            // We suspend the session queue to delay session setup until the access request has completed to avoid
            // asking the user for audio access if video access is denied.
            // Note that audio access will be implicitly requested when we create an AVCaptureDeviceInput for audio during session setup.
            sessionQueue.suspend()
            
            AVCaptureDevice.requestAccess(for: AVMediaType.video, completionHandler: { success in
                
                self.sessionQueue.resume()
                
                completionHandler(success)
            })
            
        case .denied:
            // The user has previously denied access.
            completionHandler(false)
            
        case .restricted:
            // The user doesn't have the authority to request access e.g. parental restriction.
            completionHandler(false)
            
        default:
            PBLog("Unknown authorizationStatus")
            completionHandler(false)
        }
    }
    
    private func deviceInputFor(device: AVCaptureDevice?) -> AVCaptureDeviceInput? {
        guard let validDevice = device else { return nil }
        do {
            return try AVCaptureDeviceInput(device: validDevice)
        } catch let error {
            PBLog("\(error)")
            return nil
        }
    }
    
    private func updateCameraDevice(_ cameraDevice: CameraDevice) {
        guard isCaptureSessionConfigured else { return }
        
        captureSession.beginConfiguration()
        defer { captureSession.commitConfiguration() }

        for input in captureSession.inputs {
            if let deviceInput = input as? AVCaptureDeviceInput {
                captureSession.removeInput(deviceInput)
            }
        }
        
        var captureDeviceInput = deviceInputFor(device: backCameraDevice)
        if cameraDevice == .front {
            captureDeviceInput = deviceInputFor(device: frontCameraDevice)
        }
        
        if let input = captureDeviceInput {
            if !captureSession.inputs.contains(input) {
                captureSession.addInput(input)
            }
        }
    }
    
    private var deviceOrientation: UIDeviceOrientation {
        var orientation = UIDevice.current.orientation
        if orientation == UIDeviceOrientation.unknown {
            // If UIDevice.current.orientation is unavailable (e.g. if running in an extension), use this instead.
            orientation = UIScreen.main.orientation
        }
        //PBLog("\(orientation.rawValue) UIDevice.current.orientation: \(UIDevice.current.orientation.rawValue)")
        return orientation
    }
    
    // Superimposes a white flash on the view when a photo is taken.
    private func runCaptureAnimation() {
        let captureAnimation = CABasicAnimation(keyPath: "opacity")
        captureAnimation.duration = 0.4
        captureAnimation.fromValue = 1
        captureAnimation.toValue = 0
        captureAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
        captureAnimatableLayer.add(captureAnimation, forKey: nil)
    }
    
    fileprivate func processCapturedPhoto(photo: AVCapturePhoto, captureViewSize: CGSize, maskPath: UIBezierPath?, maskImage: UIImage?, overlayImage: UIImage?, overlayFrame: CGRect?, completion: @escaping ((_ image: UIImage?) -> Void)) {
        
        // Extract the photo image data.
        guard let imageData = photo.fileDataRepresentation(), var processedImage = UIImage(data: imageData) else {
            completion(nil)
            return
        }
        
        // Scale and crop the image to match the aspect ratio of the capture view (aspect fill).
        processedImage = processedImage.scaledAndCroppedToMatchAspectRatio(of: captureViewSize)
        
        // Optionally mask the image with a path.
        if let maskCGPath = maskPath?.cgPath.mutableCopy() {
            // Scale the mask path to the image.
            let scaledPath = UIBezierPath(cgPath: maskCGPath)
            scaledPath.apply(CGAffineTransform(scaleX: processedImage.size.width / captureViewSize.width,
                                               y: processedImage.size.height / captureViewSize.height))
            if let maskedImage = processedImage.imageByApplyingClippingBezierPath(scaledPath, cropToPath: false) {
                processedImage = maskedImage
            }
        }
        
        // Optionally mask the image with a mask image.
        if let maskImage = maskImage {
            if self.cachedMaskImage?.size != processedImage.size {
                self.cachedMaskImage = maskImage.resizedTo(size: processedImage.size, using: .aspectFit)
            }
            if let scaledMaskImage = self.cachedMaskImage,
                let maskedImage = processedImage.imageMaskedBy(maskImage: scaledMaskImage) {
                //print("maskedImage: \(maskedImage.size)")
                processedImage = maskedImage
            }
        }
        
        // Optionally superimpose an overlay onto the image.
        if let overlayImage = overlayImage, let overlayFrame = overlayFrame {
            let imageFrame = CGRect(origin: CGPoint.zero, size: captureViewSize)
            processedImage = processedImage.composited(with: overlayImage,
                                                       imageFrame: imageFrame,
                                                       overlayFrame: overlayFrame,
                                                       cropToImageFrame: isOverlayImageCropped)
        }
        
        completion(processedImage)
    }

    
    // MARK: Notifications
    
    @objc
    public func onComponentViewSizeChanged() {
        // ComponentViewSizeChanged notifications serve as a proxy for UIDeviceOrientationDidChange notifications
        // when these are unavailable, as when running in an extension.
        // Only update if UIDevice.current.orientation is unknown i.e. device orientation updates are unavailable.
        guard UIDevice.current.orientation == UIDeviceOrientation.unknown else { return }
        updateForDeviceOrientation()
    }
    
    @objc
    public func updateForDeviceOrientation() {
        previewView.updateVideoOrientationFor(deviceOrientation: deviceOrientation)
    }
    
    @objc
    func sessionWasInterrupted(notification: NSNotification) {
        if let userInfoValue = notification.userInfo?[AVCaptureSessionInterruptionReasonKey] as AnyObject?,
            let reasonIntegerValue = userInfoValue.integerValue,
            let reason = AVCaptureSession.InterruptionReason(rawValue: reasonIntegerValue) {
            var reasonDescription = ""
            switch reason {
            case .videoDeviceNotAvailableInBackground:
                reasonDescription = "videoDeviceNotAvailableInBackground"
            case .audioDeviceInUseByAnotherClient:
                reasonDescription = "audioDeviceInUseByAnotherClient"
            case .videoDeviceInUseByAnotherClient:
                reasonDescription = "videoDeviceInUseByAnotherClient"
            case .videoDeviceNotAvailableWithMultipleForegroundApps:
                reasonDescription = "videoDeviceNotAvailableWithMultipleForegroundApps"
            case .videoDeviceNotAvailableDueToSystemPressure:
                reasonDescription = "videoDeviceNotAvailableDueToSystemPressure"
            default:
                PBLog("Unknown InterruptionReason: \(reason)")
            }
            PBLog("AVCaptureSession was interrupted with reason: \(reasonDescription)")
        }
    }
    
    @objc
    func sessionInterruptionEnded(notification: NSNotification) {
        PBLog("AVCaptureSession interruption ended")
    }
    
    // MARK: Methods
    
    public func startCamera() {
        
        if isCaptureSessionConfigured {
            if !self.captureSession.isRunning {
                PBLog("already configured captureSession startRunning")
                self.sessionQueue.async {
                    self.captureSession.startRunning()
                }
            }
            return
        }
        
        // First time: request camera access, configure capture session and start it.
        checkCameraAuthorization(completionHandler: { authorized in
            
            PBLog("checkCameraAuthorization \(authorized)")
            
            guard authorized else {
                PBLog("Permission to use camera denied.")
                return
            }
            
            self.sessionQueue.async {
                
                self.configureCaptureSession(completionHandler: { success in
                    guard success else { return }
                    
                    PBLog("captureSession startRunning")
                    
                    self.captureSession.startRunning()
                    
                    DispatchQueue.main.async {
                        self.updateForDeviceOrientation()
                    }
                })
            }
        })
    }
    
    public func stopCamera() {
        guard isCaptureSessionConfigured else { return }
        
        if captureSession.isRunning {
            PBLog("captureSession stopRunning")
            self.sessionQueue.async {
                self.captureSession.stopRunning()
            }
        }
    }
    
    public func takePhoto() {
        guard let capturePhotoOutput = self.capturePhotoOutput else { return }
        
        let photoSettings = AVCapturePhotoSettings()
        photoSettings.isHighResolutionPhotoEnabled = false
        photoSettings.flashMode = .off
        
        if let photoOutputVideoConnection = capturePhotoOutput.connection(with: AVMediaType.video) {
            // Match preview and photo output video orientation.
            if photoOutputVideoConnection.isVideoOrientationSupported,
                let previewViewVideoOrientation = previewView.videoOrientation {
                photoOutputVideoConnection.videoOrientation = previewViewVideoOrientation
            }
            // Mirror video if using front-facing camera.
            if photoOutputVideoConnection.isVideoMirroringSupported {
                photoOutputVideoConnection.isVideoMirrored = (cameraDevice == .front) ? true : false
            }
        }
        
        capturePhotoOutput.capturePhoto(with: photoSettings, delegate: self)
        
        // Play the capture animation.
        if isCaptureAnimationEnabled {
            DispatchQueue.main.async {
                self.runCaptureAnimation()
            }
        }
    }
    
    // Resets the camera, clearing any mask or overlay.
    public func reset() {
        clearMask()
        overlayImage = nil
        zoomFactor = 1.0
    }
}

// MARK: AVCapturePhotoCaptureDelegate
extension CameraCaptureView: AVCapturePhotoCaptureDelegate {
    
    public func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
        guard error == nil else {
            PBLog("Error capturing photo: \(error?.localizedDescription ?? "")")
            return
        }
        
        DispatchQueue.main.async {
            // Grab the necessary UIKit parameters on the main thread.
            let captureViewSize = self.bounds.size
            let maskPath = self.maskPath
            let maskImage = self.maskImage
            let overlayImage = self.overlayImage
            let overlayFrame = self.overlayImageView?.frame
        
            DispatchQueue.global(qos: .userInitiated).async {
                // Process the image on a background thread.
                self.processCapturedPhoto(photo: photo,
                                          captureViewSize: captureViewSize,
                                          maskPath: maskPath,
                                          maskImage: maskImage,
                                          overlayImage: overlayImage,
                                          overlayFrame: overlayFrame,
                                          completion: { image in
                                            
                    guard let processedImage = image else {
                        PBLog("Failed to process captured photo")
                        return
                    }
                    
                    DispatchQueue.main.async {
                        // Deliver the processed image on the main thread.
                        self.image = processedImage
                        self.didTakePhoto?(processedImage)
                    }
                })
            }
        }
    }
}
