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

import UIKit
import PlaygroundSupport
import SPCCore
import AudioToolbox

/// A space is a container component into which you can place other components. It has properties that let you customize its appearance.
/// You can subclass Space to build your own component from existing components.
///
/// - localizationKey: Space
open class Space: PlaceableComponent {
    public override class var componentType: String { return String(describing: Space.self) }
    
    public override var liveComponent: LiveComponent? {
        return liveSpace
    }
    
    private var liveSpace: LiveSpace?
    
    fileprivate enum EventName: ComponentEventName {
        case backgroundImageChanged
        case snapshotTaken
        case snapshotTakingFlashColorChanged
        case takeSnapshot
    }
    
    // MARK: Properties

    /// A background image for the component. The default is none.
    /// The background image will be resized to fill the space, preserving the aspect ratio of the image.
    /// If you want the background image to entirely fill the space, make sure the image and space have the same aspect ratio.
    ///
    /// - localizationKey: Space.backgroundImage
    public var backgroundImage: Image {
        get {
            guard let image = liveSpace?.backgroundImage else { return _backgroundImage }
            return image
        }
        set {
            _backgroundImage = newValue
            liveSpace?.backgroundImage = newValue
            updateLiveComponent(.backgroundImageChanged, value: newValue.playgroundValue)
        }
    }
    private var _backgroundImage = Image()

    /// The color of the flash that appears when you call `takeSnapshot()` to take a snapshot of the space. The default color is white.
    ///
    /// - localizationKey: Space.snapshotTakingFlashColor
    public var snapshotTakingFlashColor: Color {
        get {
            if let color = liveSpace?.snapshotTakingFlashColor {
                return color
            } else {
                return _snapshotTakingFlashColor
            }
        }
        set {
            _snapshotTakingFlashColor = newValue
            liveSpace?.snapshotTakingFlashColor = newValue
            updateLiveComponent(.snapshotTakingFlashColorChanged, value: newValue.playgroundValue)
        }
    }
    private var _snapshotTakingFlashColor = UIColor.white
    
    /// The components that have been added to the space, as an array.
    ///
    /// - localizationKey: Space.subcomponents
    public var subcomponents = [PlaceableComponent]()
    
    // MARK: SpacePlaceable
    
    /// The default size of the space if its size is not set: 1000 in width by 1000 in height.
    ///
    /// - localizationKey: Space.intrinsicSize
    open override var intrinsicSize: Size { return Size(width: 1000, height: 1000) }
    
    // MARK: Outputs

    /// An output that sends an event notification of type `Image` when a snapshot is taken of the space by calling its `takeSnapshot(includeBorder:)` method.
    ///
    /// You can connect it to an input of type `Input<Image>`, or to a function with a single parameter of type `Image`.
    ///
    /// - localizationKey: Space.snapshotTaken
    public var snapshotTaken = Output<Image>()
    
    // MARK: Initialization
    override func createLiveComponent() {
        liveSpace = LiveSpace(component: self)
    }
    
    // MARK: Methods
    
    /// Takes a snapshot image of the space, including any components that are placed in the space.
    ///
    /// - Parameter includeBorder: Set to `true` if you want the snapshot to include the border. The default is `false` — no border.
    ///
    /// - localizationKey: Space.takeSnapshot(includeBorder:)
    public func takeSnapshot(includeBorder: Bool = false) {
        if Process.isUser {
            // Send takeSnapshot event to live view.
            let event = ComponentEvent(name: EventName.takeSnapshot.rawValue, value: .boolean(includeBorder))
            event.send(to: self, in: .live)
        } else {
            // Take the snapshot.
            self.liveSpace?.takeSnapshot(includeBorder: includeBorder)
        }
    }
    
    // MARK: Messaging
    private func updateLiveComponent(_ eventName: EventName, value: PlaygroundValue?) {
        guard let playgroundValue = value else { return }
        let event = ComponentEvent(name: eventName.rawValue, value: playgroundValue)
        event.send(to: self, in: .live)
    }
    
    // MARK: ComponentEvent handling
    public override func receive(_ event: ComponentEvent, from origin: ComponentMessageOrigin) {
        super.receive(event, from: origin)
        //PBLog("event: \(event.name) sent from: \(origin) received by: \(name) (\(type(of: self))) in Space")
        
        if let _ = SpaceEventName(rawValue: event.name) {
            (self as ContainerSpace).handleSpaceEvent(event, from: origin)
        }
        
        guard let eventName = EventName(rawValue: event.name) else { return }
        switch eventName {
        case .backgroundImageChanged:
            guard let newImage = UIImage.from(event.value) as? UIImage else { return }
            liveSpace?.backgroundImage = newImage
        case .snapshotTakingFlashColorChanged:
            guard let newColor = UIColor.from(event.value) as? UIColor else { return }
            _snapshotTakingFlashColor = newColor
            liveSpace?.snapshotTakingFlashColor = newColor
        case .snapshotTaken:
            guard let newImage = UIImage.from(event.value) as? UIImage else { return }
            snapshotTaken.notifyInputs(newImage)
        case .takeSnapshot:
            guard case .boolean(let includeBorder) = event.value else { return }
            liveSpace?.takeSnapshot(includeBorder: includeBorder)
        }
    }
}

extension Space: ContainerSpace {
    
    public var liveSpaceComponent: (LiveComponent & ContainerSpace)? {
        return liveSpace
    }
    
    public var liveSubcomponents: [PlaceableComponent] {
        var placeables = [PlaceableComponent]()
        if let liveSpace = liveComponent as? LiveSpace {
            placeables += liveSpace.liveSubcomponents
        }
        return placeables
    }
}

public class LiveSpace: UIView, LiveComponent {
    public weak var component: Component?
    
    private struct SnapshotScaleInfo {
        var position: CGPoint
        var transform: CGAffineTransform
    }
    
    public var isRootSpace: Bool = false {
        didSet {
            accessibilityElements = nil
        }
    }
    private var axElement: UIAccessibilityElement?

    private var backgroundImageView: UIImageView?
    
    fileprivate var backgroundImage: Image {
        get {
            if let image = backgroundImageView?.image { return image }
            return Image()
        }
        set {
            if backgroundImageView == nil {
                let backgroundImageView = UIImageView()
                insertSubview(backgroundImageView, at: 0)
                self.backgroundImageView = backgroundImageView
                self.backgroundImageView?.contentMode = .scaleAspectFill
            }
            backgroundImageView?.image = newValue
        }
    }
    
    private static let snapshotSystemSound: UInt32 = 1057
    
    fileprivate var isSnapshotAnimationEnabled: Bool = true

    fileprivate var snapshotTakingFlashColor: UIColor = .white
    
    private var isSnapshotAnimationInProgress: Bool = false
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    required public init(component: Component) {
        self.component = component
        super.init(frame: CGRect.zero)
        backgroundColor = .clear
        clipsToBounds = true
        setAccessibilityInfo()
                
        // Define a custom AX element as a proxy for the view so it can act
        // as an AX element along with the view’s subviews—see layoutSubviews()
        let axElement = UIAccessibilityElement(accessibilityContainer: self)
        axElement.accessibilityIdentifier = self.accessibilityIdentifier
        axElement.accessibilityLabel = self.accessibilityLabel
        isAccessibilityElement = false // Accessibility container
        self.axElement = axElement
    }
    
    var layoutScale: CGFloat = 1.0
    
    var isSnapshotting = false
    
    override public func layoutSubviews() {
        super.layoutSubviews()
        for subview in subviews {
            guard
                let liveComponent = subview as? LiveComponent,
                let component = liveComponent.component as? PlaceableComponent
                else { continue }
            if subview.transform == CGAffineTransform.identity {
                subview.frame = CoordinateSystem.topLeftOriginRectFor(centerOriginPosition: component.position.cgPoint, size: component.size.cgSize, within: bounds.size)
                //PBLog("bounds \(bounds) component \(component.name) pos: \(component.position) size: \(component.size) -> rect: \(subview.frame)")
            } else {
                // If a transform is applied, frame is no applicable, so just set center.
                subview.center = CoordinateSystem.topLeftOriginCenterFor(centerOriginPosition: component.position.cgPoint, size: component.size.cgSize, within: bounds.size)
                //PBLog("center \(bounds) component \(component.name) pos: \(component.position) size: \(component.size) -> center: \(subview.center)")
            }
        }
        
        // All subviews are AX elements.
        if !isRootSpace, let axElement = axElement {
            axElement.accessibilityFrameInContainerSpace = bounds
            var elements: [Any] = [axElement]
            elements.append(subviews)
            accessibilityElements = elements
        }
        
        backgroundImageView?.frame = bounds
    }
    
    func takeSnapshot(includeBorder: Bool = false) {
        
        let takeTheSnapshot = {
            DispatchQueue.main.async {
                self.takeSnapshotUsingOffscreenRender(includeBorder: includeBorder)
            }
        }
        
        if isSnapshotAnimationEnabled {
            runSnapshotAnimationIn(self, completion: {
                takeTheSnapshot()
            })
        } else {
            takeTheSnapshot()
        }
    }
    
    private var savedSubviewScaleInfo: [ComponentIdentity: SnapshotScaleInfo]?
    
    private func adjustScaleForSnapshot(scale: CGFloat) {
        savedSubviewScaleInfo = [ComponentIdentity: SnapshotScaleInfo]()
        for subview in subviews {
            guard
                let liveComponent = subview as? LiveComponent,
                let component = liveComponent.component as? PlaceableComponent
                else { continue }
            
            savedSubviewScaleInfo?[component.identity] = SnapshotScaleInfo(position: component.position.cgPoint, transform: subview.transform)
            component.position = Point(x: component.position.x * Double(scale), y: component.position.y * Double(scale))
            subview.transform = subview.transform.scaledBy(x: scale, y: scale)
        }
        setNeedsLayout()
    }
    
    private func adjustContentScaleFactorForSnapshot(scale: CGFloat) {
        for subview in subviews {
            guard let liveComponent = subview as? LiveComponent else { continue }
            subview.contentScaleFactor = UIScreen.main.scale * scale
            if let liveSpace = liveComponent as? LiveSpace {
                liveSpace.adjustContentScaleFactorForSnapshot(scale: scale)
            }
        }
        setNeedsLayout()
    }
    
    private func restoreScaleAfterSnapshot() {
        guard let savedScaleInfo = savedSubviewScaleInfo else { return }
        for subview in subviews {
            guard
                let liveComponent = subview as? LiveComponent,
                let component = liveComponent.component as? PlaceableComponent
                else { continue }
            
            if let originalScaleInfo = savedScaleInfo[component.identity] {
                component.position = originalScaleInfo.position.point
                subview.transform = originalScaleInfo.transform
            }
        }
        setNeedsLayout()
    }
    
    private func takeSnapshotUsingOffscreenRender(includeBorder: Bool) {
        guard let superview = self.superview else { return }
        
        // Work out the size that the snapshot image needs to be.
        let availableSize = CGSize(width: 1000, height: 1000)
        let snapshotScale = bounds.size.scaleToFit(within: availableSize)
        let snapshotSize = CGSize(width: bounds.size.width * snapshotScale, height: bounds.size.height * snapshotScale)
        let snapshotFrame =  CGRect(origin: .zero, size: snapshotSize)

        let savedBounds = bounds
        let savedCenter = center
        let savedTransform = transform

        // Take a snapshot of this view and insert it above it,
        // so the view can be removed seamlessly from underneath.
        guard let coverView = snapshotView(afterScreenUpdates: true) else { return }
        coverView.bounds = bounds
        coverView.center = center
        coverView.transform = transform
        superview.insertSubview(coverView, belowSubview: self)
        
        // Create an offscreen container view and move this view (space) into it.
        let containerView = UIView(frame: CGRect(origin: CGPoint(x: 2000, y: 0), size: snapshotSize))
        superview.addSubview(containerView)
        self.removeFromSuperview()
        transform = CGAffineTransform.identity
        frame = containerView.bounds
        containerView.addSubview(self)
        
        // Scale the view (space) to the desired size for the snapshot.
        frame = snapshotFrame
        let savedBorderWith = layer.borderWidth
        if !includeBorder {
            layer.borderWidth = 0
        }
        adjustScaleForSnapshot(scale: snapshotScale)
        adjustContentScaleFactorForSnapshot(scale: snapshotScale)
        layoutIfNeeded()
        
        // Render the view (space) into an image.
        let rendererFormat = UIGraphicsImageRendererFormat()
        rendererFormat.scale = UIScreen.main.scale
        rendererFormat.opaque = false
        let renderer = UIGraphicsImageRenderer(size: snapshotSize, format: rendererFormat)
        let snapshotImage = renderer.image { context in
            drawHierarchy(in: CGRect(origin: .zero, size: snapshotSize), afterScreenUpdates: true)
        }
        
        // Restore the view (space) and remove the cover view.
        self.removeFromSuperview()
        restoreScaleAfterSnapshot()
        adjustContentScaleFactorForSnapshot(scale: 1.0)
        if !includeBorder {
            layer.borderWidth = savedBorderWith
        }
        bounds = savedBounds
        center = savedCenter
        transform = savedTransform
        superview.insertSubview(self, aboveSubview: coverView)
        coverView.removeFromSuperview()
        layoutIfNeeded()
        
        PBLog("snapshot image: \(snapshotImage.size)")
        
        // Notify that the snapshot has been taken.
        if let playgroundValue = snapshotImage.playgroundValue, let component = component {
            let event = ComponentEvent(name: Space.EventName.snapshotTaken.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
            }
        }
    }
    
    // Superimposes a white flash on the view when a snapshot is taken.
    private func runSnapshotAnimationIn(_ view: UIView, completion: @escaping (() -> Void)) {
        guard !isSnapshotAnimationInProgress else {
            // A previous snapshot animation is still running
            // => abort and don’t run completion.
            return
        }
        
        isSnapshotAnimationInProgress = true
        
        let shapeLayer = CAShapeLayer()
        shapeLayer.opacity = 0
        shapeLayer.masksToBounds = true
        shapeLayer.fillColor = snapshotTakingFlashColor.cgColor
        shapeLayer.path = UIBezierPath(rect: view.bounds).cgPath
        shapeLayer.frame = view.bounds
        view.layer.addSublayer(shapeLayer)
        
        CATransaction.begin()
        CATransaction.setCompletionBlock {
            shapeLayer.removeFromSuperlayer()
            self.isSnapshotAnimationInProgress = false
            completion()
        }

        let snapshotAnimation = CABasicAnimation(keyPath: "opacity")
        snapshotAnimation.duration = 0.25
        snapshotAnimation.fromValue = 1
        snapshotAnimation.toValue = 0
        snapshotAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
        shapeLayer.add(snapshotAnimation, forKey: nil)
        
        CATransaction.commit()
        
        AudioServicesPlaySystemSoundWithCompletion(LiveSpace.snapshotSystemSound, nil)
    }
}

extension LiveSpace: ContainerSpace {
    // Redundant in live space: included for protocol conformity.
    public var liveSpaceComponent: (LiveComponent & ContainerSpace)? {
        return nil
    }
    
    // Redundant in live space: included for protocol conformity.
    public var subcomponents: [PlaceableComponent] {
        get { return [] }
        set { }
    }
    
    // The placeable components that have been added to the live space, as an array.
    public var liveSubcomponents: [PlaceableComponent] {
        let liveComponents = subviews.compactMap { $0 as? LiveComponent }
        return liveComponents.compactMap { $0.component as? PlaceableComponent }
    }
    
    // Adds a placeable component to the live space at a specified position, and with an optional size.
    public func add(_ component: PlaceableComponent, at position: Point, size: Size? = nil) {
        guard
            let liveComponent = component.liveComponent,
            let view = liveComponent as? UIView
            else { return }
        
        let initialSize = size ?? component.intrinsicSize
        
        view.bounds = CGRect(origin: Point.zero, size: initialSize)
        view.center = CoordinateSystem.topLeftOriginCenterFor(centerOriginPosition: position.cgPoint, size: initialSize.cgSize, within: bounds.size)
        
        PBLog("live component added: \(component.name) (\(type(of: view))) to \(self.component?.name ?? "") pos: \(position) size: \(initialSize) within: \(bounds.size)  ")
        
        addSubview(view)
        setNeedsLayout()
        
        guard let sizeValue = size?.playgroundValue else { return }
        
        let event = ComponentEvent(name: "sizeChanged", value: sizeValue)
        component.receive(event, from: ComponentMessageOrigin.current)
    }
    
    // Removes a placeable component from the live space.
    public func remove(_ component: PlaceableComponent) {
        guard
            let liveComponent = component.liveComponent,
            let view = liveComponent as? UIView
            else { return }
        PBLog("live component removed: \(component.name) (\(type(of: view))) from \(self.component?.name ?? "")")
        view.removeFromSuperview()
        setNeedsLayout()
    }
    
    // Removes all components from the live space.
    public func removeAll() {
        PBLog("all live components removed from \(self.component?.name ?? "")")
        subviews.filter({ $0 is LiveComponent }).forEach({ $0.removeFromSuperview() })
    }
}


