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

import UIKit
import AVFoundation
import PlaygroundSupport
import SPCCore
import SPCComponents
import SPCLearningTrails

@objc(ComponentSpaceViewController)
public class ComponentSpaceViewController: UIViewController, PlaygroundLiveViewSafeAreaContainer {
    
    private let tableauSize = CGSize(width: 1000, height: 1000)
    private let sceneSize = CGSize(width: 800, height: 800)
    private let buttonSize = CGSize(width: 44, height: 44)
    private let compactLayoutSize = CGSize(width: 507.0, height: 364.0)
    private let buttonInset: CGFloat = 20.0
    private let learningTrailAnimationDuration = 0.4
    private let trailViewInsetsStandard = UIEdgeInsets(top: 64.0, left: 40.0, bottom: 0.0, right: 40.0)
    
    private let scrollView = UIScrollView()
    private let containerView = UIView()
    private let tableauView = UIView()
    private let gridView = GridView()
    private let liveContainerView = LiveContainerView() // Container view for Component live views.
    private var rootSpace: Space?
    private var learningTrailButton = LTBarButton()
    private var trailViewController: LearningTrailViewController?
    private var statusLabel = UILabel()
    private var connectionsLayer = CALayer()
        
    private var learningTrailDataSource = DefaultLearningTrailDataSource()
    
    private var containerViewTopConstraint: NSLayoutConstraint!
    private var containerViewBottomConstraint: NSLayoutConstraint!
    private var containerViewLeadingConstraint: NSLayoutConstraint!
    private var containerViewTrailingConstraint: NSLayoutConstraint!
    
    private var learningTrailTopConstraint: NSLayoutConstraint?
    private var learningTrailBottomConstraint: NSLayoutConstraint?
    private var learningTrailLeadingConstraint: NSLayoutConstraint?
    private var learningTrailTrailingConstraint: NSLayoutConstraint?
    
    private var learningTrailButtonTopConstraint: NSLayoutConstraint!
    private var learningTrailButtonTrailingConstraint: NSLayoutConstraint!

    private var isLearningTrailAnimationInProgress = false

    private static var cameraNotAvailableAlertShown = false
    private var hadRequestedCameraAuthorization = false
    private var shouldRequestCameraAccess = true

    // Indicates whether a Learning Trail is loaded if present.
    // Should be set before view controller is loaded.
    public var isLearningTrailEnabled = true
    
    // MARK: Initialization
    
    public convenience init(learningTrailEnabled: Bool = true) {
        self.init()
        isLearningTrailEnabled = learningTrailEnabled
    }

    public convenience init(learningTrailEnabled: Bool = true, shouldRequestCameraAccess: Bool = true) {
        self.init()
        self.isLearningTrailEnabled = learningTrailEnabled
        self.shouldRequestCameraAccess = shouldRequestCameraAccess
    }

    // MARK: View Controller Lifecycle

    override public func viewDidLoad() {
        super.viewDidLoad()
        
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        scrollView.backgroundColor = #colorLiteral(red: 0.6000000238, green: 0.6000000238, blue: 0.6000000238, alpha: 1)
        scrollView.minimumZoomScale = 0.5
        scrollView.maximumZoomScale = 4.0
        scrollView.delegate = self
        scrollView.canCancelContentTouches = false
        view.addSubview(scrollView)
        
        containerView.translatesAutoresizingMaskIntoConstraints = false
        scrollView.addSubview(containerView)

        gridView.translatesAutoresizingMaskIntoConstraints = false
        gridView.alpha = 0.3
        tableauView.addSubview(gridView)
        
        tableauView.translatesAutoresizingMaskIntoConstraints = false
        tableauView.layer.addSublayer(connectionsLayer)
        containerView.addSubview(tableauView)
        
        liveContainerView.translatesAutoresizingMaskIntoConstraints = false
        containerView.addSubview(liveContainerView)
        
        learningTrailButton.translatesAutoresizingMaskIntoConstraints = false
        let trailButtonImage = UIImage(named: "LearningTrailMaximize")?.withRenderingMode(.alwaysTemplate)
        let trailButtonSelectedImage = UIImage(named: "LearningTrailMinimize")?.withRenderingMode(.alwaysTemplate)
        learningTrailButton.setImage(trailButtonImage, for: .normal)
        learningTrailButton.setImage(trailButtonSelectedImage, for: .selected)
        learningTrailButton.addTarget(self, action: #selector(onTrailButton), for: .touchUpInside)
        learningTrailButton.isHidden = true
        learningTrailButton.tintColor = view.tintColor
        view.addSubview(learningTrailButton)
        
        statusLabel.translatesAutoresizingMaskIntoConstraints = false
        statusLabel.textColor = .yellow
        statusLabel.font = UIFont(name: "Menlo", size: 14.0)!
        statusLabel.backgroundColor = .darkGray
        statusLabel.alpha = 0.75
        statusLabel.isHidden = true
        view.addSubview(statusLabel)
        
        NSLayoutConstraint.activate([
            scrollView.topAnchor.constraint(equalTo: view.topAnchor),
            scrollView.leftAnchor.constraint(equalTo: view.leftAnchor),
            scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            scrollView.rightAnchor.constraint(equalTo: view.rightAnchor)
        ])
        
        containerViewTopConstraint = containerView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 0.0)
        containerViewBottomConstraint = containerView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 0.0)
        containerViewLeadingConstraint = containerView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 0.0)
        containerViewTrailingConstraint = containerView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: 0.0)
        
        NSLayoutConstraint.activate([
            containerViewTopConstraint,
            containerViewBottomConstraint,
            containerViewLeadingConstraint,
            containerViewTrailingConstraint,
            containerView.widthAnchor.constraint(equalToConstant: tableauSize.width),
            containerView.heightAnchor.constraint(equalToConstant: tableauSize.height)
        ])
        
        NSLayoutConstraint.activate([
            tableauView.topAnchor.constraint(equalTo: containerView.topAnchor),
            tableauView.leftAnchor.constraint(equalTo: containerView.leftAnchor),
            tableauView.widthAnchor.constraint(equalTo: containerView.widthAnchor),
            tableauView.heightAnchor.constraint(equalTo: containerView.heightAnchor)
        ])
        
        NSLayoutConstraint.activate([
            liveContainerView.topAnchor.constraint(equalTo: containerView.topAnchor),
            liveContainerView.leftAnchor.constraint(equalTo: containerView.leftAnchor),
            liveContainerView.widthAnchor.constraint(equalTo: containerView.widthAnchor),
            liveContainerView.heightAnchor.constraint(equalTo: containerView.heightAnchor)
        ])
        
        NSLayoutConstraint.activate([
            gridView.topAnchor.constraint(equalTo: tableauView.topAnchor),
            gridView.leftAnchor.constraint(equalTo: tableauView.leftAnchor),
            gridView.widthAnchor.constraint(equalTo: tableauView.widthAnchor),
            gridView.heightAnchor.constraint(equalTo: tableauView.heightAnchor)
        ])
        
        NSLayoutConstraint.activate([
            statusLabel.topAnchor.constraint(equalTo: view.topAnchor),
            statusLabel.leftAnchor.constraint(equalTo: view.leftAnchor),
            statusLabel.widthAnchor.constraint(equalTo: view.widthAnchor),
            statusLabel.heightAnchor.constraint(equalToConstant: 30.0)
        ])
        
        learningTrailButtonTopConstraint = learningTrailButton.topAnchor.constraint(equalTo: liveViewSafeAreaGuide.topAnchor, constant: 0.0)
        learningTrailButtonTrailingConstraint = learningTrailButton.trailingAnchor.constraint(equalTo: liveViewSafeAreaGuide.trailingAnchor, constant: 0.0)

        NSLayoutConstraint.activate([
            learningTrailButton.widthAnchor.constraint(equalToConstant: buttonSize.width),
            learningTrailButton.heightAnchor.constraint(equalToConstant: buttonSize.height),
            learningTrailButtonTopConstraint,
            learningTrailButtonTrailingConstraint
        ])
        
        ComponentManager.shared.isRunningInLiveView = true
        
        reset()
        
        updateLearningTrailAX()
        
        if self.shouldRequestCameraAccess {
            // Register for AVCaptureSession interruption notifications.
            NotificationCenter.default.addObserver(self, selector: #selector(onAVCaptureSessionInterrupted), name: .AVCaptureSessionWasInterrupted, object: nil)
            // Save the initial camera authorization state.
            hadRequestedCameraAuthorization = AuthorizationManager.hasRequestedCameraAuthorization
        }
        
        // Load the learning trail if there is one.
        guard isLearningTrailEnabled else { return }
        learningTrailDataSource.trail.load(completion: { success in
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                self.onLearningTrailLoaded(success)
            }
        })
    }

    override public func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self)
    }
    
    override public func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        view.setNeedsLayout()
        
        //TestSpaces.swiftyCameraWithImageView(in: self.rootSpace())
    }
    
    // MARK: Layout
    
    override public func viewDidLayoutSubviews() {
        
        // Ensure that the full height of containerView is always visible. Zoom out if necessary.
        if view.bounds.size.height < tableauSize.height {
            let zoomScale = view.bounds.size.height / tableauSize.height
            scrollView.zoomScale = zoomScale
        }
        
        // Adjust scroll offset so that containerView is horizontally centered.
        DispatchQueue.main.async {
            self.updateConstraintsForSize(self.view.bounds.size)
            let initialXOffset = (self.containerView.bounds.size.width * self.scrollView.zoomScale / 2.0) - (self.view.frame.width / 2.0)
            if initialXOffset > 0 {
                self.scrollView.contentOffset = CGPoint(x: initialXOffset, y: 0.0)
            }
        }
    }
    
    override public func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)
        
        coordinator.animate(alongsideTransition: {  _ in
        }, completion: { _ in
            self.view.setNeedsLayout()
            if Process.isLiveViewConnectionOpen {
                NotificationCenter.default.post(name: .componentViewSizeChanged, object: nil)
            }
        })
    }

    private func updateConstraintsForSize(_ size: CGSize) {
        // Keep containerView centered within scrollView.
        let yOffset: CGFloat = max(0.0, (size.height - containerView.frame.height) / 2.0)
        containerViewTopConstraint.constant = yOffset
        containerViewBottomConstraint.constant = yOffset
        let xOffset: CGFloat = max(0.0, (size.width - containerView.frame.width) / 2.0)
        containerViewLeadingConstraint.constant = xOffset
        containerViewTrailingConstraint.constant = xOffset
        
        let isCompactLayout = (liveViewSafeAreaGuide.layoutFrame.width <= compactLayoutSize.width) && (liveViewSafeAreaGuide.layoutFrame.height <= compactLayoutSize.height)
        
        // Update the position of the button(s).
        if liveViewSafeAreaGuide.layoutFrame.width > liveViewSafeAreaGuide.layoutFrame.height {
            // Landscape => buttons to the right of trail, top-aligned with top of trail.
            learningTrailTopConstraint?.constant = 0.0
            learningTrailLeadingConstraint?.constant = isCompactLayout ? 0.0 : trailViewInsetsStandard.left
            learningTrailTrailingConstraint?.constant = isCompactLayout ? 0.0 : -trailViewInsetsStandard.right
            
            learningTrailButtonTopConstraint.constant = (learningTrailTopConstraint?.constant ?? 0.0) + (trailViewController?.trailMinimumTopPadding ?? 0.0)
            learningTrailButtonTrailingConstraint.constant = -buttonInset
        } else {
            // Portrait => buttons above trail, right-aligned with trailing edge of trail.
            learningTrailTopConstraint?.constant = isCompactLayout ? 0.0 : trailViewInsetsStandard.top
            learningTrailLeadingConstraint?.constant = 0.0
            learningTrailTrailingConstraint?.constant = 0.0
            
            learningTrailButtonTopConstraint.constant = buttonInset
            learningTrailButtonTrailingConstraint.constant = -buttonInset
        }
    }

    // MARK: Actions
    
    @objc
    func onTrailButton(_ button: UIButton) {
        showLearningTrail()
    }
    
    // MARK: Notifications
    
    // Handle AVCaptureSession interruption notifications.
    @objc
    func onAVCaptureSessionInterrupted(notification: NSNotification) {
        guard let userInfoValue = notification.userInfo?[AVCaptureSessionInterruptionReasonKey] as AnyObject?,
            let reasonIntegerValue = userInfoValue.integerValue,
            let reason = AVCaptureSession.InterruptionReason(rawValue: reasonIntegerValue) else { return }
        
        guard reason == .videoDeviceNotAvailableWithMultipleForegroundApps, !hadRequestedCameraAuthorization, !(type(of: self)).cameraNotAvailableAlertShown else { return }
        
        // The camera is not available in split view or slide over multitasking modes.
        // If camera authorization hadn’t been requested before the live view was loaded (i.e. first time for this playgroundbook),
        // and if the alert hasn’t been shown before,
        // => show alert once per loaded playground book (similar to camera authorization).
        PBLog("Camera not available in multiple foreground apps: show alert.")
        let alert = UIAlertController(title: NSLocalizedString("Camera Not Available", comment: "Camera not available alert title"),
                                      message: NSLocalizedString("The camera is not available when you use Swift Playgrounds in Split View or Slide Over mode.", comment: "Camera not available alert message"),
                                      preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "OK button"), style: .cancel, handler: nil))
        present(alert, animated: true)
        (type(of: self)).cameraNotAvailableAlertShown = true
    }

    // MARK: Custom

    // Returns the current root Space. Creates and adds one if none exists.
    func rootSpace(for suppliedSpaceComponent: Space? = nil) -> Space {
        if let spaceComponent = rootSpace {
            return spaceComponent
        }
        
        // No existing rootSpace yet, so use supplied component or create one.
        var spaceComponent: Space
        if let component = suppliedSpaceComponent {
            spaceComponent = component
        } else {
            spaceComponent = Space(name: "space")
        }
        
        rootSpace = spaceComponent
        
        // Add the rootSpace's live view to the view.
        spaceComponent.position = Point.zero
        spaceComponent.size = Size(tableauSize)
        self.rootSpace = spaceComponent
        if let liveSpace = spaceComponent.liveComponent as? LiveSpace {
            //PBLog("rootSpace \(type) \(identity.name) \(identity.id)")
            liveContainerView.addSubview(liveSpace)
            liveSpace.frame = tableauView.bounds
            liveSpace.autoresizingMask = [.flexibleWidth, .flexibleHeight]
            
            liveSpace.isRootSpace = true
        }
        return spaceComponent
    }
    
    // Clears the current root Space, removing all components.
    func reset() {
        // Dismiss any presented view controllers such as an activity sheet.
        dismiss(animated: false, completion: nil)

        ComponentManager.shared.reset()
        connectionsLayer.sublayers?.forEach({ $0.removeFromSuperlayer()} )
        if let liveSpace = rootSpace?.liveComponent as? LiveSpace {
            liveSpace.removeFromSuperview()
            rootSpace = nil
        }
    }
    
    // MARK: Learning Trail
    
    private var wasLearningTrailVisibleBeforeRunMyCode = false
    
    private var learningTrailButtonShrunkenScale: CGFloat {
        return DefaultLearningStepStyle.headerButtonSize.width / buttonSize.width
    }
    
    var isLearningTrailVisible: Bool {
        return learningTrailButton.isSelected
    }
    
    func onLearningTrailLoaded(_ success: Bool) {
        // Display the Learning Trail.
        showLearningTrail()
        learningTrailButton.isHidden = false
        view.bringSubviewToFront(learningTrailButton)
        
        // Display a message if there was a problem parsing the Learning Trail XML document.
        if !success {
            var message = NSLocalizedString("⚠️ Error loading Learning Trail:", comment: "Error Message: Learning Trail loading")
            message += "\n\n"
            message += learningTrailDataSource.trail.errorMessage
            trailViewController?.showMessage(message)
        }
    }
    
    // Load and set up the trail view controller.
    func loadTrailViewController() {
        guard trailViewController == nil else { return }
        
        let trailViewController = LearningTrailViewController()
        trailViewController.learningTrailDataSource = learningTrailDataSource
        trailViewController.delegate = self
        trailViewController.view.translatesAutoresizingMaskIntoConstraints = false
        trailViewController.view.isHidden = true
        addChild(trailViewController)
        view.addSubview(trailViewController.view)
        
        let topConstraint = trailViewController.view.topAnchor.constraint(equalTo: liveViewSafeAreaGuide.topAnchor, constant: 0.0)
        let bottomConstraint = trailViewController.view.bottomAnchor.constraint(equalTo: liveViewSafeAreaGuide.bottomAnchor, constant: 0.0)
        let leadingConstraint = trailViewController.view.leftAnchor.constraint(equalTo: liveViewSafeAreaGuide.leftAnchor, constant: 0.0)
        let trailingConstraint = trailViewController.view.rightAnchor.constraint(equalTo: liveViewSafeAreaGuide.rightAnchor, constant: 0.0)
        
        NSLayoutConstraint.activate([
            topConstraint,
            bottomConstraint,
            leadingConstraint,
            trailingConstraint
            ])
        
        learningTrailTopConstraint = topConstraint
        learningTrailBottomConstraint = bottomConstraint
        learningTrailLeadingConstraint = leadingConstraint
        learningTrailTrailingConstraint = trailingConstraint
        
        self.trailViewController = trailViewController
    }
    
    func showTrailViewController() {
        guard !isLearningTrailAnimationInProgress else { return }
        guard let trailViewController = self.trailViewController else { return }
        
        isLearningTrailAnimationInProgress = true
        
        // Show the learning trail.
        let duration = learningTrailAnimationDuration
        let startPoint = view.convert(learningTrailButton.center, to: nil)
        trailViewController.show(from: startPoint, duration: duration, delay: 0.0)
        
        // Animate the learning trail button in parallel so that it lands just where the trail close button will be.
        let startPosition = view.convert(learningTrailButton.center, to: nil)
        let endPosition = trailViewController.closeButtonPosition
        let dx = endPosition.x - startPosition.x
        let dy = endPosition.y - startPosition.y
        
        UIView.animate(withDuration: duration, delay: 0.0,
                       options: [ .curveEaseOut, .beginFromCurrentState ],
                       animations: {
                        let transform = CGAffineTransform(translationX: dx, y: dy)
                        self.learningTrailButton.transform = transform
                        self.learningTrailButton.backgroundScale = self.learningTrailButtonShrunkenScale
                        self.learningTrailButton.setSelected(true, delay: duration * 0.125, duration: duration * 0.4)
        }, completion: { _ in
            self.learningTrailButton.isHidden = true
            self.learningTrailButton.transform = CGAffineTransform.identity
            self.isLearningTrailAnimationInProgress = false
            self.learningTrailButton.isSelected = true
            // Announce new state of learning trail.
            let message = NSLocalizedString("Learning Trail shown", comment: "Describes state of learning trail when it’s shown.")
            UIAccessibility.post(notification: .announcement, argument: message)
        })
    }
    
    func hideTrailViewController() {
        guard !isLearningTrailAnimationInProgress else { return }
        guard let trailViewController = self.trailViewController else { return }
        
        isLearningTrailAnimationInProgress = true
        
        // Position the learning trail button over the trail close button.
        let trailButtonPosition = view.convert(learningTrailButton.center, to: nil)
        let closeButtonPosition = trailViewController.closeButtonPosition
        let dx = closeButtonPosition.x - trailButtonPosition.x
        let dy = closeButtonPosition.y - trailButtonPosition.y
        let transform = CGAffineTransform(translationX: dx, y: dy)
        learningTrailButton.transform = transform
        learningTrailButton.backgroundScale = learningTrailButtonShrunkenScale
        learningTrailButton.isHidden = false
        
        // Hide the learning trail.
        let duration = learningTrailAnimationDuration
        let endPoint = view.convert(learningTrailButton.center, to: nil)
        trailViewController.hide(to: endPoint, duration: duration, delay: 0.0)
        
        // Animate the learning trail button in parallel back to its original position.
        UIView.animate(withDuration: duration, delay: 0.0,
                       options: [ .curveEaseOut, .beginFromCurrentState ],
                       animations: {
                        self.learningTrailButton.transform = CGAffineTransform.identity
                        self.learningTrailButton.backgroundScale = 1.0
                        self.learningTrailButton.setSelected(false, delay: 0, duration: duration * 0.4)
        }, completion: { _ in
            self.isLearningTrailAnimationInProgress = false
            self.learningTrailButton.isSelected = false
            // Announce new state of learning trail.
            let message = NSLocalizedString("Learning Trail hidden", comment: "Describes state of learning trail when it’s hidden.")
            UIAccessibility.post(notification: .announcement, argument: message)
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                UIAccessibility.post(notification: .layoutChanged, argument: self.learningTrailButton)
            }
        })
    }

    func showLearningTrail() {
        guard isLearningTrailEnabled, !isLearningTrailVisible else { return }
        
        var waitTime = 0.0
        
        if trailViewController == nil {
            loadTrailViewController()
            waitTime = 0.25 // First time: wait for trail view controller constraints to take effect visually.
        }

        DispatchQueue.main.asyncAfter(deadline: .now() + waitTime) {
            self.showTrailViewController()
            self.updateLearningTrailAX()
        }
    }
    
    func hideLearningTrail() {
        guard isLearningTrailEnabled, isLearningTrailVisible else { return }
        
        hideTrailViewController()
        updateLearningTrailAX()
    }
    
    func dismissLearningTrailPopovers() {
        // If there's one or more popovers that are presented from a trail (or its steps), dismiss them.
        trailViewController?.dismiss(animated: false, completion: nil)
    }
    
    // MARK: AX
    
    func updateLearningTrailAX() {
        learningTrailButton.accessibilityIdentifier = "\(String(describing: type(of: self))).learningTrailButton"
        learningTrailButton.accessibilityLabel = NSLocalizedString("Show Learning Trail", comment: "AX label for learning trail button when it’s hidden.")
    }
    
    // MARK: Messaging
    
    func handleCameraFunMessage(message: CameraFunMessage) {
        switch message {
        case .showGrid(origin: _):
            gridView.isHidden = false
        }
    }

    func handleComponentMessage(message: ComponentMessage) {
        switch message {
        case let .initComponent(origin: _, type: type, identity: identity):
            //PBLog("initComponent \(type) \(identity.name) \(identity.id)")
            
            // Create a component of type with the specified identity.
            guard let newComponent = PlaceableComponent.createComponent(type: type, identity: identity) else { return }
            
            // If it’s the first Space to be created, treat it as the root space.
            if rootSpace == nil, let newSpaceComponent = newComponent as? Space {
                let _ = rootSpace(for: newSpaceComponent)
            }
        case let .deinitComponent(origin: _, identity: identity):
            //PBLog("deinitComponent \(identity.description)")
            // Deregister the component.
            ComponentManager.shared.deregisterComponentWith(identity: identity)
        case let .event(origin: origin, identity: identity, event: event):
            
            // Search for component with a local identity.
            if let component = ComponentManager.shared.component(for: identity) {
                component.receive(event, from: origin)
                return
            }
        case .reset:
            reset()
        }
    }
    
    func handleLearningTrailsMessage(message: LearningTrailsMessage) {
        switch message {
        case .refreshSteps(origin: _):
            learningTrailDataSource.refreshSteps()
        }
    }
}

// MARK: UIScrollViewDelegate
extension ComponentSpaceViewController: UIScrollViewDelegate {
    
    public func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        return containerView
    }
    
    public func scrollViewDidZoom(_ scrollView: UIScrollView) {
        updateConstraintsForSize(view.bounds.size)
    }
}

// MARK: PlaygroundLiveViewMessageHandler
extension ComponentSpaceViewController: PlaygroundLiveViewMessageHandler {
    
    public func receive(_ message: PlaygroundValue) {
        if let componentMessage = ComponentMessage(playgroundValue: message) {
            handleComponentMessage(message: componentMessage)
        } else if let learningTrailsMessage = LearningTrailsMessage(playgroundValue: message) {
            handleLearningTrailsMessage(message: learningTrailsMessage)
        } else if let cameraFunMessage = CameraFunMessage(playgroundValue: message) {
            handleCameraFunMessage(message: cameraFunMessage)
        }
    }
    
    public func liveViewMessageConnectionOpened() {
        PBLog("liveViewMessageConnectionOpened")
        Process.isLiveViewConnectionOpen = true
        Process.setIsLive()
        wasLearningTrailVisibleBeforeRunMyCode = isLearningTrailVisible
        hideLearningTrail()
        dismissLearningTrailPopovers()
        scrollView.backgroundColor = #colorLiteral(red: 0.2549019754, green: 0.2745098174, blue: 0.3019607961, alpha: 1)
        gridView.isHidden = true
    }
    
    public func liveViewMessageConnectionClosed() {
        PBLog("liveViewMessageConnectionClosed")
        Process.isLiveViewConnectionOpen = false
        scrollView.backgroundColor = #colorLiteral(red: 0.6000000238, green: 0.6000000238, blue: 0.6000000238, alpha: 1)
        gridView.isHidden = false
        reset()
        // Show the learning trail again if it was visible before.
        if wasLearningTrailVisibleBeforeRunMyCode {
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
                self.showLearningTrail()
            }
        }
    }
}

// MARK: LearningTrailViewControllerDelegate
extension ComponentSpaceViewController: LearningTrailViewControllerDelegate {
    public func trailViewControllerDidRequestClose(_ trailViewController: LearningTrailViewController) {
        hideLearningTrail()
    }
}
