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

import UIKit
import PlaygroundSupport
import SPCCore

/// A camera view is a component that acts as a viewfinder, displaying the live video stream from one of the iPad’s cameras. You can use the camera view to take a photo by calling its `takePhoto()` method.
/// It has properties that let you control how the video is displayed.
/// You can use its `photoTaken` output to receive notifications when a photo has been taken.
///
/// - localizationKey: CameraView
public class CameraView: PlaceableComponent {
    public override class var componentType: String { return String(describing: CameraView.self) }
    
    public override var liveComponent: LiveComponent? {
        return liveCameraView
    }
    
    private var liveCameraView: LiveCameraView?
    
    fileprivate enum EventName: ComponentEventName {
        case isOverlayImageCroppedChanged
        case isUsingFrontCameraChanged
        case maskImageChanged
        case maskPathChanged
        case overlayImageChanged
        case overlayImageOffsetChanged
        case overlayImageScaleChanged
        case photoTaken
        case photoTakingFlashColorChanged
        case reset
        case startCamera
        case stopCamera
        case takePhoto
        case zoomChanged
    }
    
    // MARK: Properties

    /// The most recent photo (image) that has been taken by the camera view.
    ///
    /// - localizationKey: CameraView.image
    public fileprivate(set) var image = Image()
    
    /// The level of zoom applied to the video stream.
    /// The minimum value is 0.0 — fully zoomed out (the default), and the maximum value is 1.0 — fully zoomed in.
    ///
    /// - localizationKey: CameraView.zoom
    public var zoom: Double {
        get {
            guard let liveView = liveCameraView else { return _zoom }
            return liveView.zoom
        }
        set {
            _zoom = newValue
            liveCameraView?.zoom = newValue
            updateLiveComponent(.zoomChanged, value: newValue.playgroundValue)
        }
    }
    private var _zoom: Double = 1.0

    /// A boolen flag that indicates which iPad camera is being used. The default value is `false` — the rear (back) camera.
    ///
    /// - localizationKey: CameraView.isUsingFrontCamera
    public var isUsingFrontCamera: Bool {
        get {
            guard let liveView = liveCameraView else { return _isUsingFrontCamera }
            return liveView.cameraDevice == .front
        }
        set {
            _isUsingFrontCamera = newValue
            liveCameraView?.cameraDevice = newValue ? .front : .back
            updateLiveComponent(.isUsingFrontCameraChanged, value: newValue.playgroundValue)
        }
    }
    private var _isUsingFrontCamera: Bool = false
    
    /// The mask path applied to the video stream. The default is no mask path.
    /// Areas not enclosed by the mask boundaries are hidden, and will be excluded when a photo is taken.
    ///
    /// - localizationKey: CameraView.maskPath
    public var maskPath: Path {
        get {
            guard let liveView = liveCameraView else { return _maskPath }
            return liveView.maskPath ?? Path()
        }
        set {
            _maskPath = newValue
            liveCameraView?.maskPath = newValue
            updateLiveComponent(.maskPathChanged, value: newValue.playgroundValue)
        }
    }
    private var _maskPath: Path = Path()
    
    /// The mask image applied to the video stream. The default is no mask image.
    /// Transparent areas within the image are hidden, and will be excluded when a photo is taken. The level of transparency in the image’s alpha channel determines the level of opacity. Use a transparency gradient to soften the edges of the mask.
    ///
    /// - localizationKey: CameraView.maskImage
    public var maskImage: Image {
        get {
            guard let liveView = liveCameraView else { return _maskImage }
            return liveView.maskImage ?? Image()
        }
        set {
            _maskImage = newValue
            liveCameraView?.maskImage = newValue
            updateLiveComponent(.maskImageChanged, value: newValue.playgroundValue)
        }
    }
    private var _maskImage: Image = Image()
    
    /// The overlay image applied on top of the video stream. The default is no overlay image.
    /// Transparent areas within the overlay image are visible. The level in the image’s alpha channel determines the level of transparency.
    /// When a photo is taken, the overlay image is superimposed onto the photo image, so that the photo appears exactly as it did in the viewfinder.
    /// By default the overlay image is positioned in the center of the photo image and scaled (aspect fit) to the same size as the photo. However, it can be moved relative to the center by setting `overlayImageOffset` and scaled using `overlayImageScale`.
    ///
    /// - localizationKey: CameraView.overlayImage
    public var overlayImage: Image {
        get {
            guard let liveView = liveCameraView else { return _overlayImage }
            return liveView.overlayImage ?? Image()
        }
        set {
            _overlayImage = newValue
            liveCameraView?.overlayImage = newValue
            updateLiveComponent(.overlayImageChanged, value: newValue.playgroundValue)
        }
    }
    private var _overlayImage: Image = Image()
    
    /// The offset of the overlay image from the center of the camera view. The default is no offset.
    ///
    /// - localizationKey: CameraView.overlayImageOffset
    public var overlayImageOffset: Point {
        get {
            guard let liveValue = liveCameraView?.overlayImageOffset else { return _overlayImageOffset }
            return Point(x: liveValue.x, y: -liveValue.y) // UIKit -> SPCComponents coordinate space.
        }
        set {
            _overlayImageOffset = newValue
            let cgPoint = CGPoint(x: newValue.x, y: -newValue.y) // SPCComponents -> UIKit coordinate space.
            liveCameraView?.overlayImageOffset = cgPoint
            updateLiveComponent(.overlayImageOffsetChanged, value: newValue.playgroundValue)
        }
    }
    private var _overlayImageOffset = Point.zero
    
    /// The scale of the overlay image relative to the size of the camera view. The default value is 1.0.
    ///
    /// - localizationKey: CameraView.overlayImageScale
    public var overlayImageScale: Double {
        get {
            guard let liveValue = liveCameraView?.overlayImageScale else { return _overlayImageScale }
            return Double(liveValue)
        }
        set {
            _overlayImageScale = newValue
            liveCameraView?.overlayImageScale = CGFloat(newValue)
            updateLiveComponent(.overlayImageScaleChanged, value: newValue.playgroundValue)
        }
    }
    private var _overlayImageScale: Double = 1.0
    
    /// A boolean flag that indicates if the captured photo is cropped to the camera view, even if the overlay image extends outside the camera view. The default value is `true`: the captured photo will be exactly what’s visible in the camera view. If set to `false`, the entire overlay image is included in the captured photo.
    ///
    /// - localizationKey: CameraView.isOverlayImageCropped
    public var isOverlayImageCropped: Bool {
        get {
            guard let liveView = liveCameraView else { return _isOverlayImageCropped }
            return liveView.isOverlayImageCropped
        }
        set {
            _isOverlayImageCropped = newValue
            liveCameraView?.isOverlayImageCropped = newValue
            updateLiveComponent(.isOverlayImageCroppedChanged, value: newValue.playgroundValue)
        }
    }
    private var _isOverlayImageCropped: Bool = true

    /// The color of the flash animation that is briefly displayed when a photo is taken, with the default being white.
    ///
    /// - localizationKey: CameraView.photoTakingFlashColor
    public var photoTakingFlashColor: Color {
        get {
            if let color = liveCameraView?.photoTakingFlashColor {
                return color
            } else {
                return _photoTakingFlashColor
            }
        }
        set {
            _photoTakingFlashColor = newValue
            liveCameraView?.photoTakingFlashColor = newValue
            updateLiveComponent(.photoTakingFlashColorChanged, value: newValue.playgroundValue)
        }
    }
    private var _photoTakingFlashColor = UIColor()

    // SpacePlaceable
    
    /// The default size of the camera view if its size is not set: 200 in width by 200 in height.
    ///
    /// - localizationKey: CameraView.intrinsicSize
    public override var intrinsicSize: Size { return Size(width: 200, height: 200) }
    
    // MARK: Inputs
    
    /// An input that accepts event notifications of type `Pulse`. When this input receives an event, it triggers the camera view to take a photo.
    ///
    /// You can connect an output of type `Output<Pulse>` to this input. For example, you could connect the `pressed` output of a `Button` so that when you press the button a photo is taken.
    ///
    /// - localizationKey: CameraView.trigger
    public private(set) lazy var trigger = Input<Pulse>({ [weak self] _ in
        self?.takePhoto()
    })
    
    // MARK: Outputs

    /// An output that sends an event notification of type `Image` each time a photo is taken.
    ///
    /// You can connect it to an input of type `Input<Image>`, or to a function with a single parameter of type `Image`. For example, you could connect this output to the `input` input of an `ImageView` so that when a photo is taken, it appears in the image view.
    ///
    /// - localizationKey: CameraView.photoTaken
    public var photoTaken = Output<Image>()
    
    // MARK: Initialization
    
    override func createLiveComponent() {
        liveCameraView = LiveCameraView(component: self)
    }

    // MARK: Methods
    
    /// Starts the video stream from the selected iPad camera.
    ///
    /// - localizationKey: CameraView.start()
    public func start() {
        liveCameraView?.startCamera()
        
        let event = ComponentEvent(name: EventName.startCamera.rawValue, value: .boolean(true))
        event.send(to: self, in: .live)
    }
    
    /// Stops the video stream.
    ///
    /// - localizationKey: CameraView.stop()
    public func stop() {
        liveCameraView?.stopCamera()
        
        let event = ComponentEvent(name: EventName.stopCamera.rawValue, value: .boolean(true))
        event.send(to: self, in: .live)
    }
    
    /// Takes a photo.
    /// Once the photo has been taken, the `photoTaken` output sends out an event notification with the new image.
    ///
    /// - localizationKey: CameraView.takePhoto()
    public func takePhoto() {
        if Process.isUser {
            // Send takePhoto event to live view.
            let event = ComponentEvent(name: EventName.takePhoto.rawValue, value: .boolean(true))
            event.send(to: self, in: .live)
        } else {
            // Take the photo.
            liveCameraView?.takePhoto()
        }
    }
    
    /// Resets the camera view.
    /// Clears any masks and overlay, and restores the zoom level back to its default.
    ///
    /// - localizationKey: CameraView.reset()
    public func reset() {
        liveCameraView?.reset()
        
        let event = ComponentEvent(name: EventName.reset.rawValue, value: .boolean(true))
        event.send(to: self, in: .live)
    }
    
    // MARK: Messaging
    private func updateLiveComponent(_ eventName: CameraView.EventName, value: PlaygroundValue?) {
        guard let playgroundValue = value else { return }
        let event = ComponentEvent(name: eventName.rawValue, value: playgroundValue)
        event.send(to: self, in: .live)
    }
    
    public override func receive(_ event: ComponentEvent, from origin: ComponentMessageOrigin) {
        super.receive(event, from: origin)
        guard let eventName = CameraView.EventName(rawValue: event.name) else { return }
        switch eventName {
        case .photoTakingFlashColorChanged:
            guard let newColor = UIColor.from(event.value) as? UIColor else { return }
            _photoTakingFlashColor = newColor
            liveCameraView?.photoTakingFlashColor = newColor
        case .takePhoto:
            liveCameraView?.takePhoto()
        case .photoTaken:
            guard let newImage = UIImage.from(event.value) as? UIImage else { return }
            image = newImage
            photoTaken.notifyInputs(newImage)
        case .isOverlayImageCroppedChanged:
            guard case .boolean(let newValue) = event.value else { return }
            liveCameraView?.isOverlayImageCropped = newValue
        case .isUsingFrontCameraChanged:
            guard case .boolean(let newValue) = event.value else { return }
            liveCameraView?.cameraDevice = newValue ? .front : .back
        case .maskImageChanged:
            guard let newMaskImage = UIImage.from(event.value) as? UIImage else { return }
            liveCameraView?.maskImage = newMaskImage
        case .maskPathChanged:
            guard let newMaskPath = UIBezierPath.from(event.value) as? UIBezierPath else { return }
            liveCameraView?.maskPath = newMaskPath
        case .overlayImageChanged:
            guard let newOverlayImage = UIImage.from(event.value) as? UIImage else { return }
            liveCameraView?.overlayImage = newOverlayImage
        case .overlayImageOffsetChanged:
            guard let newOffset = Point.from(event.value) as? Point else { return }
            _overlayImageOffset = newOffset
            let cgPoint = CGPoint(x: newOffset.x, y: -newOffset.y) // SPCComponents -> UIKit coordinate space.
            liveCameraView?.overlayImageOffset = cgPoint
        case .overlayImageScaleChanged:
            guard case .floatingPoint(let doubleValue) = event.value else { return }
            _overlayImageScale = doubleValue
            liveCameraView?.overlayImageScale = CGFloat(doubleValue)
        case .reset:
            liveCameraView?.reset()
        case .startCamera:
            liveCameraView?.startCamera()
        case .stopCamera:
            liveCameraView?.stopCamera()
        case .zoomChanged:
            guard case .floatingPoint(let newValue) = event.value else { return }
            liveCameraView?.zoom = newValue
        }
    }
}

class LiveCameraView: CameraCaptureView, LiveComponent {
    weak var component: Component?
    
    override var maxZoomFactor: Double {
        let clampedMaxZoomFactor: Double = 4
        return min(super.maxZoomFactor, clampedMaxZoomFactor)
    }
    
    fileprivate var zoom: Double {
        get {
            guard maxZoomFactor > minZoomFactor else { return 0.0 }
            return (zoomFactor - minZoomFactor) / (maxZoomFactor - minZoomFactor)
        }
        set {
            zoomFactor = (newValue * (maxZoomFactor - minZoomFactor)) + minZoomFactor
            //PBLog("\(newValue) -> \(zoomFactor)   \(minZoomFactor), \(maxZoomFactor)")
        }
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    required init(component: Component) {
        self.component = component
        super.init(frame: CGRect.zero)
        
        isAccessibilityElement = true
        setAccessibilityInfo()
        
        didTakePhoto = { [weak self] (photoImage) in

            PBLog("didTakePhoto: \(photoImage.size.width) \(photoImage.size.height)")

            guard let component = self?.component as? CameraView else { return }
            
            component.image = photoImage

            if let playgroundValue = photoImage.playgroundValue {
                let event = ComponentEvent(name: CameraView.EventName.photoTaken.rawValue, value: playgroundValue)
                if Process.isLiveViewConnectionOpen {
                    event.send(to: component, in: Environment.user) // Update remote (user) component
                } else {
                    // Not running in a playgroundbook.
                    component.receive(event, from: ComponentMessageOrigin.current) // Update local (live) component
                }
            }
        }
    }
    
    deinit {
        PBLog("LiveCameraView")
    }
}

