#if ! __has_feature(objc_arc)
#error "ARC is off"
#endif

#import <XR.h>
#import <XRHelpers.h>

#import <UIKit/UIKit.h>
#import <ARKit/ARKit.h>
#import <ARKit/ARConfiguration.h>
#import <MetalKit/MetalKit.h>

#import "Include/IXrContextARKit.h"

@interface SessionDelegate : NSObject <ARSessionDelegate, MTKViewDelegate>
@end

namespace {
    typedef struct {
        vector_float2 position;
        vector_float2 uv;
        vector_float2 cameraUV;
    } XRVertex;

    struct ARAnchorComparer {
        bool operator()(const ARPlaneAnchor* lhs, const ARPlaneAnchor* rhs) const {
            return lhs.identifier < rhs.identifier;
        }
        bool operator()(const ARImageAnchor* lhs, const ARImageAnchor* rhs) const {
            return lhs.identifier < rhs.identifier;
        }
#if (__IPHONE_OS_VERSION_MAX_ALLOWED >= 130400)
        API_AVAILABLE(ios(13.4))
        bool operator()(const ARMeshAnchor* lhs, const ARMeshAnchor* rhs) const {
            return lhs.identifier < rhs.identifier;
        }
#endif
    };

    /**
     Defines the 2D positions and mapping UVs for both the camera and babylon textures. Camera UVs are generated by using the inverse transform of the camera image to match the display viewport.
     NOTE: For Metal UV space, 0,0 is in the top left, not the bottom left, so the V component of the UV is swapped from what you would see for OpenGL. See https://developer.apple.com/documentation/metal/creating_and_sampling_textures.
     NOTE2: Because Metal coordinates origin is different compared to OpenGL (and same as D3D), V has been flipped so rendering is not vertically flipped
     */
    static XRVertex vertices[] = {
        // 2D positions, UV,        camera UV
        { { -1, -1 },   { 0, 1 },   { 0, 0} },
        { { -1, 1 },    { 0, 0 },   { 0, 0} },
        { { 1, -1 },    { 1, 1 },   { 0, 0} },
        { { 1, 1 },     { 1, 0 },   { 0, 0} },
    };

    /**
     Defines the three-index combination that forms a unique triangle, or face. The index refers to that vertex's position in the vertices array.
     */
    struct Index3
    {
        uint32_t x, y, z;
    };

    /**
     Helper function to convert a transform into an xr::pose.
     */
    static xr::Pose TransformToPose(simd_float4x4 transform) {
        // Set orientation.
        xr::Pose pose{};
        auto orientation = simd_quaternion(transform);
        pose.Orientation = { orientation.vector.x
            , orientation.vector.y
            , orientation.vector.z
            , orientation.vector.w };

        // Set the translation.
        pose.Position = { transform.columns[3][0]
            , transform.columns[3][1]
            , transform.columns[3][2] };

        return pose;
    }

    /**
     Helper function to convert an xr pose into a transform.
     */
    static simd_float4x4 PoseToTransform(xr::Pose pose) {
        auto poseQuaternion = simd_quaternion(pose.Orientation.X, pose.Orientation.Y, pose.Orientation.Z, pose.Orientation.W);
        auto poseTransform = simd_matrix4x4(poseQuaternion);
        poseTransform.columns[3][0] = pose.Position.X;
        poseTransform.columns[3][1] = pose.Position.Y;
        poseTransform.columns[3][2] = pose.Position.Z;

        return poseTransform;
    }
}

/**
 Implementation of the ARSessionDelegate interface for more info see: https://developer.apple.com/documentation/arkit/arsessiondelegate
 */
@implementation SessionDelegate {
    std::vector<xr::System::Session::Frame::View>* activeFrameViews;

    NSLock* anchorLock;
    std::set<ARPlaneAnchor*,ARAnchorComparer> updatedPlanes;
    std::vector<ARPlaneAnchor*> deletedPlanes;
    bool planeDetectionEnabled;
#if (__IPHONE_OS_VERSION_MAX_ALLOWED >= 130400)
    API_AVAILABLE(ios(13.4))
    std::set<ARMeshAnchor*,ARAnchorComparer> updatedMeshes;
    API_AVAILABLE(ios(13.4))
    std::vector<ARMeshAnchor*> deletedMeshes;
#endif
    bool meshDetectionEnabled;
    std::set<ARImageAnchor*,ARAnchorComparer> updatedImages;
    bool imageDetectionEnabled;

    CVMetalTextureCacheRef textureCache;
    CVMetalTextureRef _cameraTextureY;
    CVMetalTextureRef _cameraTextureCbCr;
    CGSize _viewportSize;

    UIInterfaceOrientation cameraUVReferenceOrientation;
    CGSize cameraUVReferenceSize;
}

/**
 Returns the camera Y texture, the caller is responsible for freeing this texture.
 */
- (id<MTLTexture>)GetCameraTextureY {
    if (_cameraTextureY != nil) {
        id<MTLTexture> mtlTexture = CVMetalTextureGetTexture(_cameraTextureY);
        return mtlTexture;
    }

    return nil;
}

/**
 Returns the camera CbCr texture, the caller is responsible for freeing this texture.
 */
- (id<MTLTexture>)GetCameraTextureCbCr {
    if (_cameraTextureCbCr != nil) {
        id<MTLTexture> mtlTexture = CVMetalTextureGetTexture(_cameraTextureCbCr);
        return mtlTexture;
    }

    return nil;
}

/**
 Returns the set of all updated planes since the last time we consumed plane updates.
 */
- (std::set<ARPlaneAnchor*, ARAnchorComparer>*) GetUpdatedPlanes {
    return &updatedPlanes;
}

#if (__IPHONE_OS_VERSION_MAX_ALLOWED >= 130400)
/**
 Returns the set of all updated meshes since the last time we consumed mesh updates.
 */
- (std::set<ARMeshAnchor*, ARAnchorComparer>*) GetUpdatedMeshes API_AVAILABLE(ios(13.4)) {
    return &updatedMeshes;
}
#endif

/**
 Returns the set of all updated tracked images since the last time we consumed image updates.
 */
- (std::set<ARImageAnchor*, ARAnchorComparer>*) GetUpdatedImages {
    return &updatedImages;
}

/**
 Returns the vector containing all deleted planes since the last time we consumed plane updates.
 */
- (std::vector<ARPlaneAnchor*>*) GetDeletedPlanes {
    return &deletedPlanes;
}

- (void) SetPlaneDetectionEnabled:(bool)enabled {
    planeDetectionEnabled = enabled;
}

#if (__IPHONE_OS_VERSION_MAX_ALLOWED >= 130400)
- (bool) TrySetMeshDetectorEnabled:(const bool)enabled {
    if (@available(iOS 13.4, *)) {
        if([ARWorldTrackingConfiguration supportsSceneReconstruction: ARSceneReconstructionMesh]) {
            meshDetectionEnabled = enabled;
            return true;
        }
    }
    return false;
}
#endif

/**
 Returns the boolean that keeps track of whether mesh detection is enabled or not.
 */
- (bool) GetMeshDetectionEnabled {
    return meshDetectionEnabled;
}

/**
 Sets whether image detection has been enabled for this ARKit session.
 */
- (void) SetImageDetectionEnabled:(bool)enabled {
    imageDetectionEnabled = enabled;
}

/**
 Returns the boolean that keeps track of whether image detection is enabled or not.
 */
- (bool) GetImageDetectionEnabled {
    return imageDetectionEnabled;
}


#if (__IPHONE_OS_VERSION_MAX_ALLOWED >= 130400)
/**
 Returns the vector containing all deleted meshes since the last time we consumed mesh updates.
 */
- (std::vector<ARMeshAnchor*>*) GetDeletedMeshes API_AVAILABLE(ios(13.4)) {
    return &deletedMeshes;
}
#endif

/**
 Initializes this session delgate with the given frame views and metal graphics context.
 */
- (id)init:(std::vector<xr::System::Session::Frame::View>*)activeFrameViews metalContext:(id<MTLDevice>)graphicsContext {
    self = [super init];
    self->activeFrameViews = activeFrameViews;

    CVReturn err = CVMetalTextureCacheCreate(kCFAllocatorDefault, nil, graphicsContext, nil, &textureCache);
    if (err) {
        throw std::runtime_error{"Unable to create Texture Cache"};
    }

    anchorLock = [[NSLock alloc] init];
    updatedPlanes = {};
    deletedPlanes = {};
#if (__IPHONE_OS_VERSION_MAX_ALLOWED >= 130400)
    updatedMeshes = {};
    deletedMeshes = {};
#endif
    updatedImages = {};
    return self;
}

- (void) LockAnchors {
    [anchorLock lock];
}

- (void) UnlockAnchors {
    [anchorLock unlock];
}

/**
 Returns the orientation of the app
*/
- (UIInterfaceOrientation)orientation {
    UIApplication* sharedApplication = [UIApplication sharedApplication];
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_13_0)
    UIScene* scene = [[[sharedApplication connectedScenes] allObjects] firstObject];
    return [(UIWindowScene*)scene interfaceOrientation];
#else
    if (@available(iOS 13.0, *)) {
        return [[[[sharedApplication windows] firstObject] windowScene] interfaceOrientation];
    }
    else {
        return [sharedApplication statusBarOrientation];
    }
#endif
}

/**
 Returns the viewportSize as determined by the texture size of the first active frame view.
*/
- (CGSize)viewportSize {
    auto frameSize = activeFrameViews->front().ColorTextureSize;
    return CGSizeMake(frameSize.Width, frameSize.Height);
}

/**
 Implementation of the ARSessionDelegate protocol. Called every frame during the active ARKit session.
 NOTE: If this part of the protocol is implemented, then ARKit will run its own loop and push frames to this method.
      We want to pull frames on demand since we manage our own render loop. Leaving this here commented
      out to make it easy to switch back to this mode of operation or do further experimentation as needed.
*/
//- (void)session:(ARSession *) session didUpdateFrame:(ARFrame *)frame {
//    [self session:session didUpdateFrameInternal:frame];
//}

/**
 Updates the AR Camera texture, and Camera pose. If a size change is detected also sets the UVs, and FoV values.
 */
- (void)session:(ARSession *)__unused session didUpdateFrameInternal:(ARFrame *)frame {
    // Update both metal textures used by the renderer to display the camera image.
    CVMetalTextureRef newCameraTextureY = [self getCameraTexture:frame.capturedImage plane:0];
    CVMetalTextureRef newCameraTextureCbCr = [self getCameraTexture:frame.capturedImage plane:1];

    // Swap the camera textures, do this under synchronization lock to prevent null access.
    @synchronized(self) {
        [self cleanupTextures];
        _cameraTextureY = newCameraTextureY;
        _cameraTextureCbCr = newCameraTextureCbCr;
    }

    // Check if our orientation or size has changed and update camera UVs if necessary.
    if ([self checkAndUpdateCameraUVs:frame]) {
        // If our camera UVs updated, then also update the projection matrix to match the updated UVs.
        [self updateProjectionMatrix:frame.camera];
    }

    // Finally update the XR pose based on the current transform from ARKit.
    [self updateDisplayOrientedPose:(frame.camera)];
}

/**
 Updates the captured texture with the current frame buffer.
*/
- (CVMetalTextureRef)getCameraTexture:(CVPixelBufferRef)pixelBuffer plane:(int)planeIndex {
    CVReturn ret = CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
    if (ret != kCVReturnSuccess) {
        return {};
    }

    @try {
        size_t planeWidth = CVPixelBufferGetWidthOfPlane(pixelBuffer, planeIndex);
        size_t planeHeight = CVPixelBufferGetHeightOfPlane(pixelBuffer, planeIndex);

        // Plane 0 is the Y plane, which is in R8Unorm format, and the second plane is the CBCR plane which is RG8Unorm format.
        auto pixelFormat = planeIndex ? MTLPixelFormatRG8Unorm : MTLPixelFormatR8Unorm;
        CVMetalTextureRef texture;

        // Create a texture from the corresponding plane.
        auto status = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, textureCache, pixelBuffer, nil, pixelFormat, planeWidth, planeHeight, planeIndex, &texture);
        if (status != kCVReturnSuccess) {
            CVBufferRelease(texture);
            return nil;
        }

        return texture;
    }
    @finally {
        CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
    }
}

/**
 Checks whether the camera UVs need to be updated based on the orientation and size of the view port, and updates them if necessary.
 @return True if the camera UVs were updated, false otherwise.
*/
- (Boolean)checkAndUpdateCameraUVs:(ARFrame *)frame {
    // When the orientation or viewport size changes loop over triangleVerts, apply transform to the UV to generate camera UVs.
    auto orientation = [self orientation];
    CGSize viewportSize = [self viewportSize];
    if (cameraUVReferenceOrientation != orientation || cameraUVReferenceSize.height != viewportSize.height || cameraUVReferenceSize.width != viewportSize.width) {
        // The default transform is for converting normalized image coordinates to UVs, we want the inverse as we are converting
        // UVs to normalized image coordinates.
        auto transform = CGAffineTransformInvert([frame displayTransformForOrientation:orientation viewportSize:[self viewportSize]]);
        for(size_t i = 0; i < sizeof(vertices) / sizeof(*vertices); i++) {
            CGPoint transformedPoint = CGPointApplyAffineTransform({vertices[i].uv[0], vertices[i].uv[1]}, transform);
            vertices[i].cameraUV[0] = transformedPoint.x;
            vertices[i].cameraUV[1] = transformedPoint.y;
        }

        // Keep track of the last known orientation and viewport size.
        cameraUVReferenceOrientation = orientation;
        cameraUVReferenceSize = viewportSize;
        return true;
    }

    return false;
}

/**
 Gets the projection matrix of the AR Camera, and applies it to the frameView.
*/
- (void)updateProjectionMatrix:(ARCamera*)camera {
    // Get the viewport size and the orientation of the device.
    auto& frameView = activeFrameViews->at(0);
    auto viewportSize = [self viewportSize];
    auto orientation = [self orientation];

    // Grab the projection matrix for the image based on the viewport.
    auto projectionMatrix = [camera projectionMatrixForOrientation:orientation viewportSize:viewportSize zNear:frameView.DepthNearZ zFar:frameView.DepthFarZ];
    memcpy(frameView.ProjectionMatrix.data(), projectionMatrix.columns, sizeof(float) * frameView.ProjectionMatrix.size());
}

/**
 The ARKit camera transform is always a local right hand coordinate space WRT landscape right orientation, so this function takes the transform and converts
 it into a display oriented pose see: (https://developer.apple.com/documentation/arkit/arcamera/2866108-transform)
 */
-(void)updateDisplayOrientedPose:(ARCamera*)camera {
    auto& frameView = activeFrameViews->at(0);
    UIInterfaceOrientation orientation = [self orientation];
    simd_float4x4 transform = [camera transform];
    simd_quatf displayOrientationQuat;

    // Create the display orientation quaternion based on the current orientation of the device.
    if (orientation == UIInterfaceOrientationLandscapeRight) {
        displayOrientationQuat = simd_quaternion(0.0f, 0.0f, 0.0f, 1.0f);
    }
    else if (orientation == UIInterfaceOrientationLandscapeLeft) {
        displayOrientationQuat = simd_quaternion((float)M_PI, simd_make_float3(0, 0, 1));
    }
    else if (orientation == UIInterfaceOrientationPortraitUpsideDown) {
        displayOrientationQuat = simd_quaternion((float)M_PI * -.5f, simd_make_float3(0, 0, 1));
    }
    else if (orientation == UIInterfaceOrientationPortrait) {
        displayOrientationQuat = simd_quaternion((float)M_PI * .5f, simd_make_float3(0, 0, 1));
    }

    // Convert the display orientation quaternion to a transform matrix.
    simd_float4x4 rotationMatrix = simd_matrix4x4(displayOrientationQuat);

    // Multiply the transform by the rotation matrix to generate the display oriented transform.
    auto displayOrientedTransform = simd_mul(transform, rotationMatrix);

    //Pull out the display oriented rotation.
    auto displayOrientation = simd_quaternion(displayOrientedTransform);

    // Set the orientation of the camera
    frameView.Space.Pose.Orientation = { displayOrientation.vector.x
        , displayOrientation.vector.y
        , displayOrientation.vector.z
        , displayOrientation.vector.w};

    // Set the translation.
    frameView.Space.Pose.Position = { displayOrientedTransform.columns[3][0]
        , displayOrientedTransform.columns[3][1]
        , displayOrientedTransform.columns[3][2] };
}

- (void)session:(ARSession *)__unused session didAddAnchors:(nonnull NSArray<__kindof ARAnchor *> *)anchors {
    if (planeDetectionEnabled || imageDetectionEnabled || meshDetectionEnabled) {
        [self LockAnchors];
        @try {
            for (ARAnchor* newAnchor : anchors) {
                if (planeDetectionEnabled && [newAnchor isKindOfClass:[ARPlaneAnchor class]]) {
                    updatedPlanes.insert((ARPlaneAnchor*)newAnchor);
                } else if (imageDetectionEnabled && [newAnchor isKindOfClass:[ARImageAnchor class]]) {
                    updatedImages.insert((ARImageAnchor*)newAnchor);
                } else if (meshDetectionEnabled) {
#if (__IPHONE_OS_VERSION_MAX_ALLOWED >= 130400)
                    if (@available(iOS 13.4, *)) {
                        if ([newAnchor isKindOfClass:[ARMeshAnchor class]]) {
                            updatedMeshes.insert((ARMeshAnchor*)newAnchor);
                        }
                    }
#endif
                }
            }
        } @finally {
            [self UnlockAnchors];
        }
    }
    return;
}

- (void)session:(ARSession *)__unused session didUpdateAnchors:(nonnull NSArray<__kindof ARAnchor *> *)anchors {
    if (planeDetectionEnabled || imageDetectionEnabled || meshDetectionEnabled) {
        [self LockAnchors];
        @try {
            for (ARAnchor* updatedAnchor : anchors) {
                if (planeDetectionEnabled && [updatedAnchor isKindOfClass:[ARPlaneAnchor class]]) {
                    updatedPlanes.insert((ARPlaneAnchor*)updatedAnchor);
                } else if (imageDetectionEnabled && [updatedAnchor isKindOfClass:[ARImageAnchor class]]) {
                    updatedImages.insert((ARImageAnchor*)updatedAnchor);
                } else if (meshDetectionEnabled) {
#if (__IPHONE_OS_VERSION_MAX_ALLOWED >= 130400)
                    if (@available(iOS 13.4, *)) {
                        if ([updatedAnchor isKindOfClass:[ARMeshAnchor class]]) {
                            updatedMeshes.insert((ARMeshAnchor*)updatedAnchor);
                        }
                    }
#endif
                }
            }
        } @finally {
            [self UnlockAnchors];
        }
    }

    return;
}

- (void)session:(ARSession *)__unused session didRemoveAnchors:(nonnull NSArray<__kindof ARAnchor *> *)anchors {
    // Check for deleted plane or mesh anchors.
    // Note: Image anchors are never automatically deleted, so not handled here.
    if (planeDetectionEnabled || meshDetectionEnabled) {
        [self LockAnchors];
        @try {
            for (ARAnchor* deletedAnchor : anchors) {
                if (planeDetectionEnabled && [deletedAnchor isKindOfClass:[ARPlaneAnchor class]]) {
                    deletedPlanes.push_back((ARPlaneAnchor*)deletedAnchor);
                } else if (meshDetectionEnabled) {
#if (__IPHONE_OS_VERSION_MAX_ALLOWED >= 130400)
                    if (@available(iOS 13.4, *)) {
                        if ([deletedAnchor isKindOfClass:[ARImageAnchor class]]) {
                            deletedMeshes.push_back((ARMeshAnchor*)deletedAnchor);
                        }
                    }
#endif
                }
            }
        } @finally {
            [self UnlockAnchors];
        }
    }

    return;
}

-(void)cleanupTextures {
    if (_cameraTextureY != nil) {
        CVBufferRelease(_cameraTextureY);
        _cameraTextureY = nil;
    }

    if (_cameraTextureCbCr != nil) {
        CVBufferRelease(_cameraTextureCbCr);
        _cameraTextureCbCr = nil;
    }
}

-(void)dealloc {
  [self cleanupTextures];

  if (textureCache != nil) {
      CVMetalTextureCacheFlush(textureCache, 0);
      CFRelease(textureCache);
      textureCache = nil;
  }
}

- (void)mtkView:(MTKView *)__unused view drawableSizeWillChange:(CGSize)size {
    _viewportSize.width = size.width;
    _viewportSize.height = size.height;
}

- (void)drawInMTKView:(MTKView *)__unused view {
}

- (CGSize)viewSize {
    return _viewportSize;
}

- (void)setViewSize:(CGSize)size {
    _viewportSize = size;
}

@end
namespace xr {
    namespace {
        // This shader is used to render *either* the camera texture or the final composited texture.
        // It could be split into two shaders, but this is a bit simpler since they use the same structs
        // and have some common logic.
        // The shader is used in two passes:
        // 1. Render the camera texture to the color render texture (see GetNextFrame).
        // 2. Render the composited texture to the screen (see DrawFrame).
        constexpr char shaderSource[] = R"(
            #include <metal_stdlib>
            #include <simd/simd.h>

            using namespace metal;

            #include <simd/simd.h>

            typedef struct
            {
                vector_float2 position;
                vector_float2 uv;
                vector_float2 cameraUV;
            } XRVertex;

            typedef struct
            {
                float4 position [[position]];
                float2 uv;
                float2 cameraUV;
            } RasterizerData;

            vertex RasterizerData
            vertexShader(uint vertexID [[vertex_id]],
                         constant XRVertex *vertices [[buffer(0)]])
            {
                RasterizerData out;
                out.position = vector_float4(vertices[vertexID].position.xy, 0.0, 1.0);
                out.uv = vertices[vertexID].uv;
                out.cameraUV = vertices[vertexID].cameraUV;
                return out;
            }

            fragment float4 fragmentShader(RasterizerData in [[stage_in]],
                texture2d<float, access::sample> babylonTexture [[ texture(0) ]],
                texture2d<float, access::sample> cameraTextureY [[ texture(1) ]],
                texture2d<float, access::sample> cameraTextureCbCr [[ texture(2) ]])
            {
                constexpr sampler linearSampler(mip_filter::linear, mag_filter::linear, min_filter::linear);

                if (!is_null_texture(babylonTexture))
                {
                    return babylonTexture.sample(linearSampler, in.uv);
                }
                else if (!is_null_texture(cameraTextureY) && !is_null_texture(cameraTextureCbCr))
                {
                    const float4 cameraSampleY = cameraTextureY.sample(linearSampler, in.cameraUV);
                    const float4 cameraSampleCbCr = cameraTextureCbCr.sample(linearSampler, in.cameraUV);

                    const float4x4 ycbcrToRGBTransform = float4x4(
                        float4(+1.0000f, +1.0000f, +1.0000f, +0.0000f),
                        float4(+0.0000f, -0.3441f, +1.7720f, +0.0000f),
                        float4(+1.4020f, -0.7141f, +0.0000f, +0.0000f),
                        float4(-0.7010f, +0.5291f, -0.8860f, +1.0000f)
                    );

                    float4 ycbcr = float4(cameraSampleY.r, cameraSampleCbCr.rg, 1.0);
                    float4 cameraSample = ycbcrToRGBTransform * ycbcr;
                    cameraSample.a = 1.0;

                    return cameraSample;
                }
                else
                {
                    return 0;
                }
            }
        )";

        id<MTLLibrary> CompileShader(id<MTLDevice> metalDevice, const char* source) {
            NSError* error;
            id<MTLLibrary> lib = [metalDevice newLibraryWithSource:@(source) options:nil error:&error];
            if(nil != error) {
                throw std::runtime_error{[error.localizedDescription cStringUsingEncoding:NSASCIIStringEncoding]};
            }
            return lib;
        }
    }

    struct XrContextARKit : public IXrContextARKit {
        bool Initialized{true};
        ARSession* Session{nullptr};
        ARFrame* Frame{nullptr};

        bool IsInitialized() const override
        {
            return Initialized;
        }

        ARSession* XrSession() const override
        {
            return Session;
        }

        ARFrame* XrFrame() const override
        {
            return Frame;
        }

        virtual ~XrContextARKit() = default;
    };

    struct System::Impl {
    public:
        std::unique_ptr<XrContextARKit> XrContext{std::make_unique<XrContextARKit>()};

        Impl(const std::string&) {}

        bool IsInitialized() const {
            return XrContext->IsInitialized();
        }

        bool TryInitialize() {
            return true;
        }
    };

    struct System::Session::Impl {
    public:
        const System::Impl& SystemImpl;
        std::vector<Frame::View> ActiveFrameViews{ {} };
        std::vector<Frame::InputSource> InputSources;
        std::vector<Frame::Plane> Planes{};
        std::vector<Frame::Mesh> Meshes{};
        std::vector<std::unique_ptr<Frame::ImageTrackingResult>> ImageTrackingResults{};
        std::vector<FeaturePoint> FeaturePointCloud{};
        std::optional<Space> EyeTrackerSpace{};
        float DepthNearZ{ DEFAULT_DEPTH_NEAR_Z };
        float DepthFarZ{ DEFAULT_DEPTH_FAR_Z };
        bool FeaturePointCloudEnabled{ false };

        Impl(System::Impl& systemImpl, void* graphicsContext, void* commandQueue, std::function<void*()> windowProvider)
            : SystemImpl{ systemImpl }
            , getXRView{ [windowProvider{ std::move(windowProvider) }] { return (__bridge MTKView*)windowProvider(); } }
            , metalDevice{ (__bridge id<MTLDevice>)graphicsContext }
            , commandQueue{ (__bridge id<MTLCommandQueue>)commandQueue } {

            // Create the ARSession enable plane detection, include scene reconstruction mesh if supported, and disable lighting estimation.
            SystemImpl.XrContext->Session = [ARSession new];
            auto configuration = [ARWorldTrackingConfiguration new];
#if (__IPHONE_OS_VERSION_MAX_ALLOWED >= 130400)
            if (@available(iOS 13.4, *)) {
                if ([ARWorldTrackingConfiguration supportsSceneReconstruction: ARSceneReconstructionMesh]) {
                    configuration.sceneReconstruction = ARSceneReconstructionMesh;
                }
            }
#endif
            configuration.planeDetection = ARPlaneDetectionHorizontal | ARPlaneDetectionVertical;
            configuration.lightEstimationEnabled = false;
            configuration.worldAlignment = ARWorldAlignmentGravity;

            sessionDelegate = [[SessionDelegate new]init:&ActiveFrameViews metalContext:metalDevice];
            SystemImpl.XrContext->Session.delegate = sessionDelegate;

            UpdateXRView();

            [SystemImpl.XrContext->Session runWithConfiguration:configuration];

            id<MTLLibrary> lib = CompileShader(metalDevice, shaderSource);
            id<MTLFunction> vertexFunction = [lib newFunctionWithName:@"vertexShader"];
            id<MTLFunction> fragmentFunction = [lib newFunctionWithName:@"fragmentShader"];

            // Create a pipeline state for drawing the camera texture to the render target texture.
            {
                MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
                pipelineStateDescriptor.label = @"XR Camera Pipeline";
                pipelineStateDescriptor.vertexFunction = vertexFunction;
                pipelineStateDescriptor.fragmentFunction = fragmentFunction;
                pipelineStateDescriptor.colorAttachments[0].pixelFormat = MTLPixelFormatBGRA8Unorm;
                pipelineStateDescriptor.depthAttachmentPixelFormat = MTLPixelFormatDepth32Float_Stencil8;
                pipelineStateDescriptor.stencilAttachmentPixelFormat = MTLPixelFormatDepth32Float_Stencil8;

                NSError* error;
                cameraPipelineState = [metalDevice newRenderPipelineStateWithDescriptor:pipelineStateDescriptor error:&error];
                if (!cameraPipelineState) {
                    NSLog(@"Failed to create camera pipeline state: %@", error);
                }
            }

            // Create a pipeline state for drawing the final composited texture to the screen.
            {
                MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
                pipelineStateDescriptor.label = @"XR Screen Pipeline";
                pipelineStateDescriptor.vertexFunction = vertexFunction;
                pipelineStateDescriptor.fragmentFunction = fragmentFunction;
                pipelineStateDescriptor.colorAttachments[0].pixelFormat = MTLPixelFormatBGRA8Unorm;

                NSError* error;
                screenPipelineState = [metalDevice newRenderPipelineStateWithDescriptor:pipelineStateDescriptor error:&error];
                if (!screenPipelineState) {
                    NSLog(@"Failed to create screen pipeline state: %@", error);
                }
            }
        }

        ~Impl() {
            if (currentCommandBuffer != nil) {
                [currentCommandBuffer waitUntilCompleted];
            }

            if (ActiveFrameViews[0].ColorTexturePointer != nil) {
                id<MTLTexture> oldColorTexture = (__bridge_transfer id<MTLTexture>)ActiveFrameViews[0].ColorTexturePointer;
                [oldColorTexture setPurgeableState:MTLPurgeableStateEmpty];
                ActiveFrameViews[0].ColorTexturePointer = nil;
            }

            if (ActiveFrameViews[0].DepthTexturePointer != nil) {
                id<MTLTexture> oldDepthTexture = (__bridge_transfer id<MTLTexture>)ActiveFrameViews[0].DepthTexturePointer;
                [oldDepthTexture setPurgeableState:MTLPurgeableStateEmpty];
                ActiveFrameViews[0].DepthTexturePointer = nil;
            }

            Planes.clear();
            Meshes.clear();
            CleanupAnchor(nil);
            [SystemImpl.XrContext->Session pause];
            UpdateXRView(nil);
        }

        void UpdateXRView() {
            UpdateXRView(getXRView());
        }

        void UpdateXRView(MTKView* activeXRView) {
            // Check whether the xr view has changed, and if so, reconfigure it.
            if (activeXRView != xrView) {
                if (xrView) {
                    xrView.delegate = nil;
                    [xrView releaseDrawables];
                    metalLayer = nil;
                }

                xrView = activeXRView;

                if (xrView) {
                    xrView.colorPixelFormat = MTLPixelFormatBGRA8Unorm;
// NOTE: There is an incorrect warning about CAMetalLayer specifically when compiling for the simulator.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wpartial-availability"
                    metalLayer = (CAMetalLayer *)xrView.layer;
#pragma clang diagnostic pop
                    metalLayer.device = metalDevice;

                    auto scale = xrView.contentScaleFactor;
                    viewportSize.x = xrView.bounds.size.width * scale;
                    viewportSize.y = xrView.bounds.size.height * scale;
                    [sessionDelegate setViewSize:CGSizeMake(viewportSize.x, viewportSize.y)];

                    xrView.delegate = sessionDelegate;
                }
            }
        }

        /**
         After the ARSession starts, it takes a little time before AR frames become available. This function just makes it easy to roll this into CreateAsync.
         */
        arcana::task<void, std::exception_ptr> WhenReady() {
            __block arcana::task_completion_source<void, std::exception_ptr> tcs;
            CFRunLoopRef mainRunLoop = CFRunLoopGetMain();
            const auto intervalInSeconds = 0.033;
            CFRunLoopTimerRef timer = CFRunLoopTimerCreateWithHandler(kCFAllocatorDefault, CFAbsoluteTimeGetCurrent(), intervalInSeconds, 0, 0, ^(CFRunLoopTimerRef timer){
                if ([SystemImpl.XrContext->Session currentFrame] != nil) {
                    CFRunLoopRemoveTimer(mainRunLoop, timer, kCFRunLoopCommonModes);
                    CFRelease(timer);
                    tcs.complete();
                }
            });
            CFRunLoopAddTimer(mainRunLoop, timer, kCFRunLoopCommonModes);
            return tcs.as_task();
        }

        std::unique_ptr<System::Session::Frame> GetNextFrame(bool& shouldEndSession, bool& shouldRestartSession, std::function<arcana::task<void, std::exception_ptr>(void*)> deletedTextureAsyncCallback) {
            shouldEndSession = sessionEnded;
            shouldRestartSession = false;

            SystemImpl.XrContext->Frame = SystemImpl.XrContext->Session.currentFrame;

            UpdateXRView();

            [sessionDelegate session:SystemImpl.XrContext->Session didUpdateFrameInternal:SystemImpl.XrContext->Frame];

            auto viewSize = [sessionDelegate viewSize];
            viewportSize.x = viewSize.width;
            viewportSize.y = viewSize.height;
            uint32_t width = viewportSize.x;
            uint32_t height = viewportSize.y;

            if (ActiveFrameViews[0].ColorTextureSize.Width != width || ActiveFrameViews[0].ColorTextureSize.Height != height) {
                // Color texture
                {
                    if (ActiveFrameViews[0].ColorTexturePointer != nil) {
                        id<MTLTexture> oldColorTexture = (__bridge_transfer id<MTLTexture>)ActiveFrameViews[0].ColorTexturePointer;
                        deletedTextureAsyncCallback(ActiveFrameViews[0].ColorTexturePointer).then(arcana::inline_scheduler, arcana::cancellation::none(), [oldColorTexture]() {
                            [oldColorTexture setPurgeableState:MTLPurgeableStateEmpty];
                        });
                        ActiveFrameViews[0].ColorTexturePointer = nil;
                    }

                    MTLTextureDescriptor *textureDescriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm width:width height:height mipmapped:NO];
                    textureDescriptor.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead;
                    id<MTLTexture> texture = [metalDevice newTextureWithDescriptor:textureDescriptor];

                    ActiveFrameViews[0].ColorTexturePointer = (__bridge_retained void*)texture;
                    ActiveFrameViews[0].ColorTextureFormat = TextureFormat::BGRA8_SRGB;
                    ActiveFrameViews[0].ColorTextureSize = {width, height};
                }

                // Allocate and store the depth texture
                {
                    if (ActiveFrameViews[0].DepthTexturePointer != nil) {
                        id<MTLTexture> oldDepthTexture = (__bridge_transfer id<MTLTexture>)ActiveFrameViews[0].DepthTexturePointer;
                        deletedTextureAsyncCallback(ActiveFrameViews[0].DepthTexturePointer).then(arcana::inline_scheduler, arcana::cancellation::none(), [oldDepthTexture]() {
                            [oldDepthTexture setPurgeableState:MTLPurgeableStateEmpty];
                        });
                        ActiveFrameViews[0].DepthTexturePointer = nil;
                    }

                    MTLTextureDescriptor *textureDescriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatDepth32Float_Stencil8 width:width height:height mipmapped:NO];
                    textureDescriptor.storageMode = MTLStorageModePrivate;
                    textureDescriptor.usage = MTLTextureUsageRenderTarget;
                    id<MTLTexture> texture = [metalDevice newTextureWithDescriptor:textureDescriptor];

                    ActiveFrameViews[0].DepthTexturePointer = (__bridge_retained void*)texture;
                    ActiveFrameViews[0].DepthTextureFormat = TextureFormat::D24S8;
                    ActiveFrameViews[0].DepthTextureSize = {width, height};
                }
            }

            // Draw the camera texture to the color texture and clear the depth texture before handing them off to Babylon.
            currentCommandBuffer = [commandQueue commandBuffer];
            currentCommandBuffer.label = @"XRCameraCommandBuffer";
            MTLRenderPassDescriptor *renderPassDescriptor = [MTLRenderPassDescriptor renderPassDescriptor];

            id<MTLTexture> cameraTextureY = nil;
            id<MTLTexture> cameraTextureCbCr = nil;
            @synchronized(sessionDelegate) {
                cameraTextureY = [sessionDelegate GetCameraTextureY];
                cameraTextureCbCr = [sessionDelegate GetCameraTextureCbCr];
            }

            if(renderPassDescriptor != nil) {
                // Attach the color texture, on which we'll draw the camera texture (so no need to clear on load).
                renderPassDescriptor.colorAttachments[0].texture = (__bridge id<MTLTexture>)ActiveFrameViews[0].ColorTexturePointer;
                renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionDontCare;
                renderPassDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore;

                // Attach the depth texture, which should be cleared on load.
                renderPassDescriptor.depthAttachment.texture = (__bridge id<MTLTexture>)ActiveFrameViews[0].DepthTexturePointer;
                renderPassDescriptor.depthAttachment.loadAction = MTLLoadActionClear;
                renderPassDescriptor.depthAttachment.storeAction = MTLStoreActionStore;

                // Attach the stencil texture, which should be cleared on load.
                renderPassDescriptor.stencilAttachment.texture = (__bridge id<MTLTexture>)ActiveFrameViews[0].DepthTexturePointer;
                renderPassDescriptor.stencilAttachment.loadAction = MTLLoadActionClear;
                renderPassDescriptor.stencilAttachment.storeAction = MTLStoreActionStore;

                // Create and end the render encoder.
                id<MTLRenderCommandEncoder> renderEncoder = [currentCommandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
                renderEncoder.label = @"XRCameraEncoder";

                // Set the shader pipeline.
                [renderEncoder setRenderPipelineState:cameraPipelineState];

                // Set the vertex data.
                [renderEncoder setVertexBytes:vertices length:sizeof(vertices) atIndex:0];

                // Set the textures.
                [renderEncoder setFragmentTexture:cameraTextureY atIndex:1];
                [renderEncoder setFragmentTexture:cameraTextureCbCr atIndex:2];

                // Draw the triangles.
                [renderEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4];

                [renderEncoder endEncoding];
                
                [currentCommandBuffer addCompletedHandler:^(id<MTLCommandBuffer>) {
                    if (cameraTextureY != nil) {
                        [cameraTextureY setPurgeableState:MTLPurgeableStateEmpty];
                    }

                    if (cameraTextureCbCr != nil) {
                        [cameraTextureCbCr setPurgeableState:MTLPurgeableStateEmpty];
                    }
                }];
            }

            // Finalize rendering here & push the command buffer to the GPU.
            [currentCommandBuffer commit];

            return std::make_unique<Frame>(*this);
        }

        void RequestEndSession() {
            // Note the end session has been requested, and respond to the request in the next call to GetNextFrame
            sessionEnded = true;
        }

        void DrawFrame() {
            if (metalLayer) {
                // Create a new command buffer for each render pass to the current drawable.
                currentCommandBuffer = [commandQueue commandBuffer];
                currentCommandBuffer.label = @"XRScreenCommandBuffer";

                id<CAMetalDrawable> drawable = [metalLayer nextDrawable];
                MTLRenderPassDescriptor *renderPassDescriptor = [MTLRenderPassDescriptor renderPassDescriptor];

                if (renderPassDescriptor != nil) {
                    renderPassDescriptor.colorAttachments[0].texture = drawable.texture;
                    renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionDontCare;

                    // Create a render command encoder.
                    id<MTLRenderCommandEncoder> renderEncoder = [currentCommandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
                    renderEncoder.label = @"XRScreenEncoder";

                    // Set the region of the drawable to draw into.
                    [renderEncoder setViewport:(MTLViewport){0.0, 0.0, static_cast<double>(viewportSize.x), static_cast<double>(viewportSize.y), 0.0, 1.0 }];

                    // Set the shader pipeline.
                    [renderEncoder setRenderPipelineState:screenPipelineState];

                    // Set the vertex data.
                    [renderEncoder setVertexBytes:vertices length:sizeof(vertices) atIndex:0];

                    // Set the textures.
                    [renderEncoder setFragmentTexture:(__bridge id<MTLTexture>)ActiveFrameViews[0].ColorTexturePointer atIndex:0];

                    // Draw the triangles.
                    [renderEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4];

                    [renderEncoder endEncoding];

                    // Schedule a present once the framebuffer is complete using the current drawable.
                    [currentCommandBuffer presentDrawable:drawable];
                }

                // Finalize rendering here & push the command buffer to the GPU.
                [currentCommandBuffer commit];
            }

            if (SystemImpl.XrContext->Frame != nil) {
                SystemImpl.XrContext->Frame = nil;
            }
        }

        void GetHitTestResults(std::vector<HitResult>& filteredResults, xr::Ray offsetRay, xr::HitTestTrackableType trackableTypes) const {
            if (SystemImpl.XrContext->Frame != nil && SystemImpl.XrContext->Frame.camera != nil && [SystemImpl.XrContext->Frame.camera trackingState] == ARTrackingStateNormal) {
                if (@available(iOS 13.0, *)) {
                    GetHitTestResultsForiOS13(filteredResults, offsetRay, trackableTypes);
                } else {
#if (__IPHONE_OS_VERSION_MIN_REQUIRED <= __IPHONE_13_0)
                    GetHitTestResultsLegacy(filteredResults, trackableTypes);
#endif
                }
            }
        }

        /**
         Create an ARKit anchor for the given pose.
         */
        xr::Anchor CreateAnchor(Pose pose) {
            // Pull out the pose into a float 4x4 transform that is usable by ARKit.
            auto poseTransform = PoseToTransform(pose);

            // Create the anchor and add it to the ARKit session.
            auto anchor = [[ARAnchor alloc] initWithTransform:poseTransform];
            [SystemImpl.XrContext->Session addAnchor:anchor];
            nativeAnchors.push_back(anchor);
            return { { pose }, (__bridge NativeAnchorPtr)anchor };
        }

        /**
         Declares an ARKit anchor that was created outside the BabylonNative xr system.
         */
        xr::Anchor DeclareAnchor(NativeAnchorPtr anchor)
        {
            const auto arAnchor = (__bridge ARAnchor*)anchor;
            nativeAnchors.push_back(arAnchor);
            const auto pose{TransformToPose(arAnchor.transform)};
            return { { pose }, anchor };
        }
        
        /**
         For a given anchor update the current pose, and determine if it is still valid.
         */
        void UpdateAnchor(xr::Anchor& anchor) {
            // First check if the anchor still exists, if not then mark the anchor as no longer valid.
            auto arAnchor = (__bridge ARAnchor*)anchor.NativeAnchor;
            if (arAnchor == nil) {
                anchor.IsValid = false;
                return;
            }

            // Then update the anchor's pose based on its transform.
            anchor.Space.Pose = TransformToPose(arAnchor.transform);
        }

        /**
         Deletes the ArKit anchor associated with this XR anchor if it still exists.
         */
        void DeleteAnchor(xr::Anchor& anchor) {
            // If this anchor has not already been deleted, then remove it from the current AR session,
            // and clean up its state in memory.
            if (anchor.NativeAnchor != nil) {
                auto arAnchor = (__bridge ARAnchor*)anchor.NativeAnchor;
                anchor.NativeAnchor = nil;

                CleanupAnchor(arAnchor);
            }
        }

        /**
         Updates existing planes in place, gets the list of updated/created plane IDs, and removed plane IDs.
         */
        void UpdatePlanes(std::vector<Frame::Plane::Identifier>& updatedPlanes, std::vector<Frame::Plane::Identifier>& deletedPlanes) {
            if (!planeDetectionEnabled) {
                return;
            }

            [sessionDelegate LockAnchors];
            @try {
                // First lets go and update all planes that have been updated since the last frame.
                auto updatedARKitPlanes = [sessionDelegate GetUpdatedPlanes];
                for (ARPlaneAnchor* updatedPlane : *updatedARKitPlanes) {
                    // Dynamically allocate the polygon array, and fill it in.
                    auto geometry = updatedPlane.geometry;
                    auto polygonSize = geometry.boundaryVertexCount;

                    planePolygonBuffer.clear();
                    planePolygonBuffer.resize(polygonSize * 3);
                    for (NSUInteger i = 0; i < polygonSize; i++) {
                        NSUInteger polygonIndex =  i * 3;
                        planePolygonBuffer[polygonIndex] = geometry.boundaryVertices[i].x;
                        planePolygonBuffer[polygonIndex + 1] = geometry.boundaryVertices[i].y;
                        planePolygonBuffer[polygonIndex + 2] = geometry.boundaryVertices[i].z;
                    }

                    // Update the existing plane if it exists, otherwise create a new plane, and add it to our list of planes.
                    auto planeIterator = planeMap.find({[updatedPlane.identifier.UUIDString UTF8String]});
                    if (planeIterator != planeMap.end()) {
                        UpdatePlane(updatedPlanes, GetPlaneByID(planeIterator->second), updatedPlane, planePolygonBuffer, polygonSize);
                    } else {
                        // This is a new plane, create it and initialize its values.
                        Planes.emplace_back();
                        auto& plane = Planes.back();
                        planeMap.insert({{[updatedPlane.identifier.UUIDString UTF8String]}, plane.ID});

                        // Fill in the polygon and center pose.
                        UpdatePlane(updatedPlanes, plane, updatedPlane, planePolygonBuffer, polygonSize);
                    }
                }

                // Clear the list of updated planes to start building up for the next frame update.
                updatedARKitPlanes->clear();

                // Now loop over all deleted planes find them in the existing planes map and if the entry exists add it to the list of removed planes.
                auto removedARKitPlanes = [sessionDelegate GetDeletedPlanes];
                for (ARPlaneAnchor* removedPlane: *removedARKitPlanes) {
                    // Find the plane in the set of existing planes.
                    auto planeIterator = planeMap.find({[removedPlane.identifier.UUIDString UTF8String]});
                    if (planeIterator != planeMap.end()) {
                        // Release the held ref to the native plane ID and clean up its polygon as it is no longer needed.
                        auto [nativePlaneID, planeID] = *planeIterator;
                        deletedPlanes.push_back(planeID);

                        auto& plane = GetPlaneByID(planeID);
                        plane.Polygon.clear();
                        plane.PolygonSize = 0;
                        planeMap.erase(planeIterator);
                    }
                }

                // Clear the list of removed frames to start building up for the next plane update.
                removedARKitPlanes->clear();
            } @finally {
                [sessionDelegate UnlockAnchors];
            }
        }
        
        /**
         Updates existing image anchors in place, gets the list of updated/created image IDs.
         */
        void UpdateImageTrackingResults(std::vector<Frame::ImageTrackingResult::Identifier>& updatedImageTrackingResults) {
            if (![sessionDelegate GetImageDetectionEnabled]) {
                return;
            }

            [sessionDelegate LockAnchors];
            @try {
                // Iterate over all newly found/updated image anchors and either create or update the image tracking result.
                auto updatedARImages{[sessionDelegate GetUpdatedImages]};
                for (ARImageAnchor* updatedImage : *updatedARImages) {
                    // Update the existing image tracking result if it exists, otherwise create a new result and add it to our list of results.
                    const auto resultIterator{imageTrackingMap.find({[updatedImage.identifier.UUIDString UTF8String]})};
                    if (resultIterator != imageTrackingMap.end()) {
                        UpdateImageTrackingResult(updatedImageTrackingResults, GetImageTrackingResultByID(resultIterator->second), updatedImage);
                    } else {
                        // This is a new result. Create it and initialize its values.
                        imageTrackingResults.push_back(std::make_unique<Frame::ImageTrackingResult>());
                        auto& result{ *imageTrackingResults.back() };
                        result.Index = [updatedImage.referenceImage.name intValue];
                        imageTrackingMap.insert({[updatedImage.identifier.UUIDString UTF8String], result.ID});
                        UpdateImageTrackingResult(updatedImageTrackingResults, result, updatedImage);
                    }
                }
                
                // Clear the list of updated images to start building up for the next frame update.
                updatedARImages->clear();
            } @finally {
                [sessionDelegate UnlockAnchors];
            }
        }

#if (__IPHONE_OS_VERSION_MAX_ALLOWED >= 130400)
        /**
         Updates existing meshes in place, gets the list of updated/created mesh IDs and removed mesh IDs.
         */
        void UpdateMeshes(std::vector<Frame::Mesh::Identifier>& updatedMeshes, std::vector<Frame::Mesh::Identifier>& deletedMeshes) {
            if (![sessionDelegate GetMeshDetectionEnabled]) {
                return;
            }
            if (@available(iOS 13.4, *)) {
                [sessionDelegate LockAnchors];
                @try {
                    // Update all meshes that have been updated since the last frame
                    auto updatedARKitMeshes = [sessionDelegate GetUpdatedMeshes];
                    for (ARMeshAnchor* updatedMesh : *updatedARKitMeshes) {
                        const auto& geometry{ updatedMesh.geometry };
                        const auto& faces{ geometry.faces };
                        const auto& vertices{ geometry.vertices };
                        const auto& normals{ geometry.normals };
                        // get vertices data for vertex buffer
                        meshVertexBuffer.clear();
                        meshVertexBuffer.resize(vertices.count);
                        std::vector<Vector3f> vertexData{};
                        vertexData.resize(vertices.count);
                        memcpy(vertexData.data(), vertices.buffer.contents, vertices.stride * vertices.count);
                        for(int i = 0; i < vertices.count; i++) {
                            const auto vertex = vertexData[i];
                            auto vertexTransform = matrix_identity_float4x4;
                            vertexTransform.columns[3] = simd_float4{vertex.X, vertex.Y, vertex.Z, 1};
                            auto transform = matrix_multiply(updatedMesh.transform, vertexTransform);
                            const auto position = simd_float3{transform.columns[3].x ,transform.columns[3].y, transform.columns[3].z};
                            meshVertexBuffer[i].X = position[0];
                            meshVertexBuffer[i].Y = position[1];
                            meshVertexBuffer[i].Z = position[2];
                        }
                        // get indices data for index buffer
                        const auto indexCount = faces.count * faces.indexCountPerPrimitive;
                        meshIndexBuffer.clear();
                        meshIndexBuffer.resize(indexCount);
                        std::vector<Index3> indexData{};
                        indexData.resize(faces.count);
                        memcpy(indexData.data(), faces.buffer.contents, faces.bytesPerIndex * faces.count * faces.indexCountPerPrimitive);
                        auto index = 0;
                        for(int i=0; i<faces.count; i++){
                            meshIndexBuffer[index] = indexData[i].x;
                            meshIndexBuffer[index + 1] = indexData[i].y;
                            meshIndexBuffer[index + 2] = indexData[i].z;
                            index+=3;
                        }
                        // get normals data for normals buffer
                        meshNormalsBuffer.clear();
                        meshNormalsBuffer.resize(normals.count);
                        std::vector<Vector3f> normalsData{};
                        normalsData.resize(normals.count);
                        memcpy(normalsData.data(), normals.buffer.contents, normals.stride * normals.count);
                        for(int i = 0; i < normals.count; i++) {
                            const auto normal = normalsData[i];
                            auto normalTransform = matrix_identity_float4x4;
                            normalTransform.columns[3] = simd_float4{normal.X, normal.Y, normal.Z, 1};
                            auto meshTranspose = simd_transpose(simd_inverse(updatedMesh.transform));
                            auto transform = matrix_multiply(meshTranspose, normalTransform);
                            const auto position = simd_float3{transform.columns[3].x ,transform.columns[3].y, transform.columns[3].z};
                            meshNormalsBuffer[i].X = position[0];
                            meshNormalsBuffer[i].Y = position[1];
                            meshNormalsBuffer[i].Z = position[2];
                        }
                        // Update the existing mesh if it exists, otherwise create a new mesh and add it to our list of meshes.
                        auto meshIterator = meshMap.find({[updatedMesh.identifier.UUIDString UTF8String]});
                        if (meshIterator != meshMap.end()) {
                            // if meshID was found, update values
                            UpdateMesh(updatedMeshes, GetMeshByID(meshIterator->second), meshVertexBuffer, meshIndexBuffer, meshNormalsBuffer);
                        } else {
                            // create new mesh and initialize its values.
                            Meshes.emplace_back();
                            auto& mesh = Meshes.back();
                            meshMap.insert({{[updatedMesh.identifier.UUIDString UTF8String]}, mesh.ID});
                            // Fill in the mesh info
                            UpdateMesh(updatedMeshes, mesh, meshVertexBuffer, meshIndexBuffer, meshNormalsBuffer);
                        }
                    }
                    // Clear the list of updated meshes to start building up for the next frame update.
                    updatedARKitMeshes->clear();

                    // Now loop over all deleted meshes, find them in the existing meshes map, and if the entry exists add it to the list of removed meshes
                    auto removedARKitMeshes = [sessionDelegate GetDeletedMeshes];
                    for (ARMeshAnchor* removedMesh: *removedARKitMeshes) {
                        // Find the mesh in the set of existing meshes.
                        auto meshIterator = meshMap.find({[removedMesh.identifier.UUIDString UTF8String]});
                        if (meshIterator != meshMap.end()) {
                            // Release the held ref to the native mesh ID and clean up its data as it is no longer needed.
                            auto [nativeMeshID, meshID] = *meshIterator;
                            deletedMeshes.push_back(meshID);
                            auto& mesh = GetMeshByID(meshID);
                            mesh.Positions.clear();
                            mesh.Indices.clear();
                            mesh.Normals.clear();
                            meshMap.erase(meshIterator);
                        }
                    }

                    // Clear the list of removed frames to start building up for the next mesh update.
                    removedARKitMeshes->clear();
                } @finally {
                    [sessionDelegate UnlockAnchors];
                }
            }
        }
#endif

        void UpdateFeaturePointCloud() {
            if (!FeaturePointCloudEnabled) {
                return;
            }

            ARPointCloud* pointCloud = SystemImpl.XrContext->Frame.rawFeaturePoints;

            FeaturePointCloud.resize(pointCloud.count);
            for (NSUInteger i = 0; i < pointCloud.count; i++) {
                FeaturePointCloud.emplace_back();
                auto& featurePoint { FeaturePointCloud.back() };

                // Grab the position from the point cloud.
                // Reflect the point across the Z axis, as we want to report this
                // value in camera space.
                featurePoint.X = pointCloud.points[i][0];
                featurePoint.Y = pointCloud.points[i][1];
                featurePoint.Z = -1 * pointCloud.points[i][2];

                // ARKit feature points don't have confidence values, so just default to 1.0f
                featurePoint.ConfidenceValue = 1.0f;

                // Check to see if this point ID exists in our point cloud mapping if not add it to the map.
                const uint64_t id { pointCloud.identifiers[i] };
                auto featurePointIterator = featurePointIDMap.find(id);
                if (featurePointIterator != featurePointIDMap.end()) {
                    featurePoint.ID = featurePointIterator->second;
                } else {
                    featurePoint.ID = nextFeaturePointID++;
                    featurePointIDMap.insert({id, featurePoint.ID});
                }
            }
        }

        Frame::Plane& GetPlaneByID(Frame::Plane::Identifier planeID)
        {
            const auto end{Planes.end()};
            const auto it{std::find_if(
                Planes.begin(),
                end,
                [&](Frame::Plane& plane) { return plane.ID == planeID; })};
            if (it != end) {
                return *it;
            } else {
                throw std::runtime_error{"Tried to get non-existent plane."};
            }
        }

        Frame::Mesh& GetMeshByID(Frame::Mesh::Identifier meshID)
        {
            const auto end{Meshes.end()};
            const auto it{std::find_if(
                Meshes.begin(),
                end,
                [&](Frame::Mesh& mesh) { return mesh.ID == meshID; })};
            if (it != end) {
                return *it;
            } else {
                throw std::runtime_error{"Tried to get non-existent mesh."};
            }
        }
        
        Frame::ImageTrackingResult& GetImageTrackingResultByID(Frame::ImageTrackingResult::Identifier resultID) {
            const auto end{ImageTrackingResults.end()};
            const auto it{std::find_if(
                imageTrackingResults.begin(),
                end,
                [&](std::unique_ptr<Frame::ImageTrackingResult>& resultPtr) { return resultPtr->ID == resultID; })};
            if (it != end) {
                return **it;
            } else {
                throw std::runtime_error{"Tried to get non-existent image tracking result."};
            }
        }

        /**
         Deallocates the native ARKit anchor object, and removes it from the anchor list.
         */
        void CleanupAnchor(ARAnchor* arAnchor) {
            // Iterate over the list of anchors if arAnchor is nil then clean up all anchors
            // otherwise clean up only the target anchor and return.
            auto anchorIter{nativeAnchors.begin()};
            while (anchorIter != nativeAnchors.end()) {
                if (arAnchor == nil || arAnchor == *anchorIter) {
                    [SystemImpl.XrContext->Session removeAnchor:*anchorIter];
                    anchorIter = nativeAnchors.erase(anchorIter);

                    if (arAnchor != nil) {
                        return;
                    }
                }
                else {
                    anchorIter++;
                }
            }
        }

        void SetPlaneDetectionEnabled(bool enabled)
        {
            planeDetectionEnabled = enabled;
            [sessionDelegate SetPlaneDetectionEnabled:enabled];
        }

#if (__IPHONE_OS_VERSION_MAX_ALLOWED >= 130400)
        bool TrySetMeshDetectorEnabled(const bool enabled)
        {
            return [sessionDelegate TrySetMeshDetectorEnabled:enabled];
        }
#endif

        bool IsTracking() const {
            // There are three different tracking states as defined in ARKit: https://developer.apple.com/documentation/arkit/artrackingstate
            // From my testing even while obscuring the camera for a long duration the state still registers as ARTrackingStateLimited
            // rather than ARTrackingStateNotAvailable. For that reason the only state that should be considered to be trully tracking is
            // ARTrackingStateNormal.
            return SystemImpl.XrContext->Frame.camera.trackingState == ARTrackingState::ARTrackingStateNormal;
        }
        
        std::vector<ImageTrackingScore>* GetImageTrackingScores() {
            if (imageTrackingScoresValid) {
                return &imageTrackingScores;
            } else {
                return nil;
            }
        }
        
        void CreateAugmentedImageDatabase(const std::vector<ImageTrackingRequest>& requests) {
            if (requests.size() == 0) {
                return;
            }

            // Create and resize vectors to hold request results.
            std::vector<arcana::task<ARReferenceImage*, std::exception_ptr>> validationTasks{};
            validationTasks.resize(requests.size());
            imageTrackingScores.resize(requests.size());

            // Loop over every requested image, and add it to the image database.
            for (size_t i{0}; i < requests.size(); i++) {
                const ImageTrackingRequest& request{requests[i]};
                
                // Convert each image request from a bitmap to a CGImage, and prepare to pass that to the ARKit configuration.
                const size_t imageBytes{request.stride * request.height};
                const size_t pixelStride{request.stride / request.width};
                const size_t bitsPerComponent{static_cast<size_t>(pixelStride == 2 || pixelStride == 6 || pixelStride == 8 ? 16 : 8)};
                const CGColorSpaceRef colorSpace{pixelStride > 2 ? CGColorSpaceCreateDeviceRGB() : CGColorSpaceCreateDeviceGray()};
                const CGDataProviderRef provider{CGDataProviderCreateWithData(nil, request.data, imageBytes, nil)};
                const CGImageRef image{
                    CGImageCreate(
                       request.width,
                       request.height,
                       bitsPerComponent,
                       8 * pixelStride /* bitsPerPixel */,
                       request.stride,
                       colorSpace,
                       CGBitmapInfo(pixelStride == 4 || pixelStride == 8 ? kCGImageAlphaNoneSkipLast : 0),
                       provider,
                       nil /* decode buffer */,
                       true /* shouldInterpolate */,
                       CGColorRenderingIntent::kCGRenderingIntentDefault)
                };

                // Create the AR Reference image.
                ARReferenceImage* referenceImage{[[ARReferenceImage alloc]
                    initWithCGImage: image
                    orientation: CGImagePropertyOrientation::kCGImagePropertyOrientationUp
                    physicalWidth: request.measuredWidthInMeters == 0 ? 1 : request.measuredWidthInMeters]};
                
                // Store the index in the name field.
                referenceImage.name = [NSString stringWithFormat:@"%zu", i];
                
                // Queue image validation
                __block arcana::task_completion_source<ARReferenceImage*, std::exception_ptr> tcs{};
                validationTasks[i] = tcs.as_task();
                if (@available(iOS 13.0, *)) {
                    [referenceImage validateWithCompletionHandler:^(NSError * _Nullable error) {
                        if (error != nil) {
                                imageTrackingScores[i] = ImageTrackingScore::UNTRACKABLE;
                                tcs.complete(nullptr);
                            } else {
                                imageTrackingScores[i] = ImageTrackingScore::TRACKABLE;
                                
                                // Add image to our image set if it is trackable.
                                tcs.complete(referenceImage);
                            }
                    }];
                } else {
                    imageTrackingScores[i] = ImageTrackingScore::TRACKABLE;
                    tcs.complete(referenceImage);
                }
                
                CGImageRelease(image);
                CGDataProviderRelease(provider);
                CGColorSpaceRelease(colorSpace);
            }
                
            // Wait for all scores to calculated on a separate scheduler.
            arcana::when_all(gsl::make_span(validationTasks))
                .then(arcana::inline_scheduler, arcana::cancellation::none(), [this](std::vector<ARReferenceImage*> referenceImages) {
                    size_t imageCount{0};
                    NSMutableSet<ARReferenceImage*>* imageSet{[NSMutableSet<ARReferenceImage*> setWithCapacity:imageTrackingScores.size()]};
                    for (ARReferenceImage* referenceImage : referenceImages) {
                        if (referenceImage != nullptr) {
                            [imageSet addObject: referenceImage];
                            imageCount++;
                        }
                    }
                                       
                    // If we have any images that qualified for tracking then enable image detection.
                    ARWorldTrackingConfiguration* configuration{static_cast<ARWorldTrackingConfiguration*>(SystemImpl.XrContext->Session.configuration)};
                    if (imageCount > 0 && configuration != nil) {
                        configuration.detectionImages = imageSet;
                        
                        if (@available(iOS 13.0, *)) {
                            configuration.automaticImageScaleEstimationEnabled = true;
                        }

                        // Sets the max number of frequently updated image anchors up to the ARKit cap of 4.
                        // Any additional images will be tracked infrequently at 1 tick every 1-2 seconds.
                        // See: https://developer.apple.com/documentation/arkit/arworldtrackingconfiguration/2968182-maximumnumberoftrackedimages
                        configuration.maximumNumberOfTrackedImages = imageCount > 4 ? 4 : imageCount;
                        [SystemImpl.XrContext->Session runWithConfiguration: configuration];
                        [sessionDelegate SetImageDetectionEnabled:true];
                        imageTrackingScoresValid = true;
                    }
            });
        }

    private:
        std::function<MTKView*()> getXRView{};
        MTKView* xrView{};
        bool sessionEnded{ false };
        id<MTLDevice> metalDevice{};
// NOTE: There is an incorrect warning about CAMetalLayer specifically when compiling for the simulator.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wpartial-availability"
        CAMetalLayer* metalLayer{};
#pragma clang diagnostic pop
        SessionDelegate* sessionDelegate{};
        id<MTLRenderPipelineState> cameraPipelineState{};
        id<MTLRenderPipelineState> screenPipelineState{};
        vector_uint2 viewportSize{};
        id<MTLCommandQueue> commandQueue;
        id<MTLCommandBuffer> currentCommandBuffer;
        std::vector<ARAnchor*> nativeAnchors{};
        std::vector<float> planePolygonBuffer{};
        std::vector<Vector3f> meshVertexBuffer{};
        std::vector<uint32_t> meshIndexBuffer{};
        std::vector<Vector3f> meshNormalsBuffer{};
        std::unordered_map<std::string, Frame::Plane::Identifier> planeMap{};
        std::unordered_map<std::string, Frame::Mesh::Identifier> meshMap{};
        std::unordered_map<uint64_t, FeaturePoint::Identifier> featurePointIDMap{};
        
        bool imageTrackingScoresValid{ false };
        std::vector<ImageTrackingScore> imageTrackingScores{};
        std::vector<std::unique_ptr<Frame::ImageTrackingResult>> imageTrackingResults{};
        std::unordered_map<std::string, Frame::ImageTrackingResult::Identifier> imageTrackingMap{};
        
        FeaturePoint::Identifier nextFeaturePointID{};
        bool planeDetectionEnabled{ false };

        /*
         Helper function to translate a world transform into a hit test result.
         */
        HitResult transformToHitResult(simd_float4x4 transform) const {
            auto orientation = simd_quaternion(transform);
            HitResult hitResult{};
            hitResult.Pose.Orientation = {
                orientation.vector.x,
                orientation.vector.y,
                orientation.vector.z,
                orientation.vector.w
            };
            hitResult.Pose.Position = {
                transform.columns[3][0],
                transform.columns[3][1],
                transform.columns[3][2]
            };

            return hitResult;
        }

        void UpdatePlane(std::vector<Frame::Plane::Identifier>& updatedPlanes, Frame::Plane& plane, ARPlaneAnchor* planeAnchor, std::vector<float>& newPolygon, size_t polygonSize) {
            Pose newCenter = TransformToPose(planeAnchor.transform);

            // If the plane was not actually updated, free the polygon buffer, and return.
            if (!CheckIfPlaneWasUpdated(plane, newPolygon, newCenter)) {
                return;
            }

            // Update the center of the plane
            plane.Center = newCenter;

            // Store the polygon.
            plane.Polygon.swap(newPolygon);
            plane.PolygonSize = polygonSize;
            plane.PolygonFormat = PolygonFormat::XYZ;
            updatedPlanes.push_back(plane.ID);
        }
        
        void UpdateImageTrackingResult(std::vector<Frame::ImageTrackingResult::Identifier>& updatedImageTrackingResults, Frame::ImageTrackingResult& imageTrackingResult, ARImageAnchor* imageAnchor) {
            Pose newPose{TransformToPose(imageAnchor.transform)};

            // Update the pose for the image
            imageTrackingResult.ImageSpace.Pose = newPose;
            
            // Update the estimated size.
            if (@available(iOS 13.0, *)) {
                imageTrackingResult.MeasuredWidthInMeters = imageAnchor.estimatedScaleFactor * imageAnchor.referenceImage.physicalSize.width;
            } else {
                imageTrackingResult.MeasuredWidthInMeters = imageAnchor.referenceImage.physicalSize.width;
            }
            
            // Update tracking state
            imageTrackingResult.TrackingState = imageAnchor.isTracked
                ? ImageTrackingState::TRACKED
                : ImageTrackingState::EMULATED;
            updatedImageTrackingResults.push_back(imageTrackingResult.ID);
        }

        void UpdateMesh(std::vector<Frame::Mesh::Identifier>& updatedMeshes, Frame::Mesh& mesh, std::vector<Vector3f>& vertexBuffer, std::vector<uint32_t>& indexBuffer, std::vector<Vector3f>& normalsBuffer) {
            // Check if mesh was actually updated
            bool checkIfMeshUpdated{ false };
            if (mesh.Positions.size() != vertexBuffer.size()) {
                checkIfMeshUpdated = true;
            } else {
                int compare =  memcmp(mesh.Positions.data(), vertexBuffer.data(), vertexBuffer.size() * sizeof(Vector3f));
                if (compare != 0) {
                    checkIfMeshUpdated = true;
                }
            }

            if (checkIfMeshUpdated) {
                // Store the new mesh information
                mesh.Positions.resize(vertexBuffer.size());
                mesh.Positions.swap(vertexBuffer);
                static_assert(sizeof(Frame::Mesh::IndexType) == sizeof(uint32_t));
                mesh.Indices.resize(indexBuffer.size());
                memcpy(mesh.Indices.data(), indexBuffer.data(), indexBuffer.size() * sizeof(uint32_t));
                mesh.HasNormals = true;
                mesh.Normals.resize(normalsBuffer.size());
                mesh.Normals.swap(normalsBuffer);
                updatedMeshes.push_back(mesh.ID);
            }
        }

        // For iOS 13.0 and up make use of the ARRaycastQuery protocol for raycasting against all target trackable types.
        API_AVAILABLE(ios(13.0))
        void GetHitTestResultsForiOS13(std::vector<HitResult>& filteredResults, xr::Ray offsetRay, xr::HitTestTrackableType trackableTypes) const{
            // Push the camera origin into a simd_float3.
            auto cameraOrigin = simd_make_float3(
                                                 ActiveFrameViews[0].Space.Pose.Position.X,
                                                 ActiveFrameViews[0].Space.Pose.Position.Y,
                                                 ActiveFrameViews[0].Space.Pose.Position.Z);

            // Push the camera direction into a simd_quaternion.
            auto cameraDirection = simd_quaternion(
                                                   ActiveFrameViews[0].Space.Pose.Orientation.X,
                                                   ActiveFrameViews[0].Space.Pose.Orientation.Y,
                                                   ActiveFrameViews[0].Space.Pose.Orientation.Z,
                                                   ActiveFrameViews[0].Space.Pose.Orientation.W);

            // Load the offset ray and direction into simd equivalents.
            auto offsetOrigin = simd_make_float3(offsetRay.Origin.X, offsetRay.Origin.Y, offsetRay.Origin.Z);
            auto offsetDirection = simd_make_float3(offsetRay.Direction.X, offsetRay.Direction.Y, offsetRay.Direction.Z);
            auto rayOrigin = cameraOrigin + offsetOrigin;
            auto rayDirection = simd_act(cameraDirection, offsetDirection);

            // Check which types we are meant to raycast against and perform their respective queries.
            if ((trackableTypes & xr::HitTestTrackableType::MESH) != xr::HitTestTrackableType::NONE) {
                PerformRaycastQueryAgainstTarget(filteredResults, ARRaycastTargetExistingPlaneGeometry, rayOrigin, rayDirection);
            }

            if ((trackableTypes & xr::HitTestTrackableType::POINT) != xr::HitTestTrackableType::NONE) {
                PerformRaycastQueryAgainstTarget(filteredResults, ARRaycastTargetEstimatedPlane, rayOrigin, rayDirection);
            }

            if ((trackableTypes & xr::HitTestTrackableType::PLANE) != xr::HitTestTrackableType::NONE) {
                PerformRaycastQueryAgainstTarget(filteredResults, ARRaycastTargetExistingPlaneInfinite, rayOrigin, rayDirection);
            }
        }

        API_AVAILABLE(ios(13.0))
        void PerformRaycastQueryAgainstTarget(std::vector<HitResult>& filteredResults, ARRaycastTarget targetType, simd_float3 origin, simd_float3 direction) const {
            auto raycastQuery = [[ARRaycastQuery alloc]
                                 initWithOrigin:origin
                                 direction:direction
                                 allowingTarget:targetType
                                 alignment:ARRaycastTargetAlignmentAny];

            // Perform the actual raycast.
            auto rayCastResults = [SystemImpl.XrContext->Session raycast:raycastQuery];

            // Process the results and push them into the results list.
            for (ARRaycastResult* result in rayCastResults) {
                filteredResults.push_back(transformToHitResult(result.worldTransform));
            }
        }

#if (__IPHONE_OS_VERSION_MIN_REQUIRED <= __IPHONE_13_0)
        // On iOS versions prior to 13, fall back to doing a raycast from a screen point, for now don't support translating the offset ray.
        void GetHitTestResultsLegacy(std::vector<HitResult>& filteredResults, xr::HitTestTrackableType trackableTypes) const {
            // First set the type filter based on the requested trackable types.
            ARHitTestResultType typeFilter = 0;
            if ((trackableTypes & xr::HitTestTrackableType::POINT) != xr::HitTestTrackableType::NONE) {
                typeFilter |= ARHitTestResultTypeFeaturePoint;
            }

            if ((trackableTypes & xr::HitTestTrackableType::PLANE) != xr::HitTestTrackableType::NONE) {
                typeFilter |= ARHitTestResultTypeExistingPlane;
            }

            if ((trackableTypes & xr::HitTestTrackableType::POINT) != xr::HitTestTrackableType::NONE) {
                if (@available(iOS 11.3, *)) {
                    typeFilter |= ARHitTestResultTypeExistingPlaneUsingGeometry;
                } else {
                    typeFilter |= ARHitTestResultTypeExistingPlaneUsingExtent;
                }
            }

            // Now perform the actual hit test and process the results
            auto hitTestResults = [SystemImpl.XrContext->Frame hitTest:CGPointMake(.5, .5) types:(typeFilter)];
            for (ARHitTestResult* result in hitTestResults) {
                filteredResults.push_back(transformToHitResult(result.worldTransform));
            }
        }
#endif
    };

    struct System::Session::Frame::Impl {
    public:
        Impl(Session::Impl& sessionImpl)
            : sessionImpl{sessionImpl} { }

        Session::Impl& sessionImpl;
    };

    System::Session::Frame::Frame(Session::Impl& sessionImpl)
        : Views{ sessionImpl.ActiveFrameViews }
        , InputSources{ sessionImpl.InputSources}
        , FeaturePointCloud{ sessionImpl.FeaturePointCloud }
        , EyeTrackerSpace{ sessionImpl.EyeTrackerSpace }
        , UpdatedPlanes{}
        , RemovedPlanes{}
        , UpdatedMeshes{}
        , RemovedMeshes{}
        , UpdatedImageTrackingResults{}
        , IsTracking{sessionImpl.IsTracking()}
        , m_impl{ std::make_unique<System::Session::Frame::Impl>(sessionImpl) } {
        Views[0].DepthNearZ = sessionImpl.DepthNearZ;
        Views[0].DepthFarZ = sessionImpl.DepthFarZ;
        m_impl->sessionImpl.UpdatePlanes(UpdatedPlanes, RemovedPlanes);
#if (__IPHONE_OS_VERSION_MAX_ALLOWED >= 130400)
        m_impl->sessionImpl.UpdateMeshes(UpdatedMeshes, RemovedMeshes);
#endif
        m_impl->sessionImpl.UpdateImageTrackingResults(UpdatedImageTrackingResults);
        m_impl->sessionImpl.UpdateFeaturePointCloud();
    }

    System::Session::Frame::~Frame() {
    }

    void System::Session::Frame::Render() {
        m_impl->sessionImpl.DrawFrame();
    }

    void System::Session::Frame::GetHitTestResults(std::vector<HitResult>& filteredResults, xr::Ray offsetRay, xr::HitTestTrackableType trackableTypes) const {
        m_impl->sessionImpl.GetHitTestResults(filteredResults, offsetRay, trackableTypes);
    }

    Anchor System::Session::Frame::CreateAnchor(Pose pose, NativeTrackablePtr) const {
        return m_impl->sessionImpl.CreateAnchor(pose);
    }

    Anchor System::Session::Frame::DeclareAnchor(NativeAnchorPtr anchor) const {
        return m_impl->sessionImpl.DeclareAnchor(anchor);
    }

    void System::Session::Frame::UpdateAnchor(xr::Anchor& anchor) const {
        m_impl->sessionImpl.UpdateAnchor(anchor);
    }

    void System::Session::Frame::DeleteAnchor(xr::Anchor& anchor) const {
        m_impl->sessionImpl.DeleteAnchor(anchor);
    }

    System::Session::Frame::SceneObject& System::Session::Frame::GetSceneObjectByID(System::Session::Frame::SceneObject::Identifier) const {
        throw std::runtime_error("not implemented");
    }

    System::Session::Frame::Plane& System::Session::Frame::GetPlaneByID(System::Session::Frame::Plane::Identifier planeID) const {
        return m_impl->sessionImpl.GetPlaneByID(planeID);
    }

    System::Session::Frame::ImageTrackingResult& System::Session::Frame::GetImageTrackingResultByID(System::Session::Frame::ImageTrackingResult::Identifier resultID) const {
        return m_impl->sessionImpl.GetImageTrackingResultByID(resultID);
    }

    System::Session::Frame::Mesh& System::Session::Frame::GetMeshByID(System::Session::Frame::Mesh::Identifier meshID) const {
        return m_impl->sessionImpl.GetMeshByID(meshID);
    }

    System::System(const char* appName)
        : m_impl{ std::make_unique<System::Impl>(appName) } {}

    System::~System() {}

    bool System::IsInitialized() const {
        return m_impl->IsInitialized();
    }

    bool System::TryInitialize() {
        return m_impl->TryInitialize();
    }

    arcana::task<bool, std::exception_ptr> System::IsSessionSupportedAsync(SessionType sessionType) {
        // Only IMMERSIVE_AR is supported for now.
        return arcana::task_from_result<std::exception_ptr>(sessionType == SessionType::IMMERSIVE_AR && ARWorldTrackingConfiguration.isSupported);
    }

    uintptr_t System::GetNativeXrContext()
    {
        return reinterpret_cast<uintptr_t>(m_impl->XrContext.get());
    }

    std::string System::GetNativeXrContextType()
    {
        return "ARKit";
    }

    arcana::task<std::shared_ptr<System::Session>, std::exception_ptr> System::Session::CreateAsync(System& system, void* graphicsDevice, void* commandQueue, std::function<void*()> windowProvider) {
        auto session = std::make_shared<System::Session>(system, graphicsDevice, commandQueue, std::move(windowProvider));
        return session->m_impl->WhenReady().then(arcana::inline_scheduler, arcana::cancellation::none(), [session] {
            return session;
        });
    }

    System::Session::Session(System& system, void* graphicsDevice, void* commandQueue, std::function<void*()> windowProvider)
        : m_impl{ std::make_unique<System::Session::Impl>(*system.m_impl, graphicsDevice, commandQueue, std::move(windowProvider)) } {}

    System::Session::~Session() {
        // Free textures
    }

    std::unique_ptr<System::Session::Frame> System::Session::GetNextFrame(bool& shouldEndSession, bool& shouldRestartSession, std::function<arcana::task<void, std::exception_ptr>(void*)> deletedTextureAsyncCallback) {
        return m_impl->GetNextFrame(shouldEndSession, shouldRestartSession, deletedTextureAsyncCallback);
    }

    void System::Session::RequestEndSession() {
        m_impl->RequestEndSession();
    }

    void System::Session::SetDepthsNearFar(float depthNear, float depthFar) {
        m_impl->DepthNearZ = depthNear;
        m_impl->DepthFarZ = depthFar;
    }

    void System::Session::SetPlaneDetectionEnabled(bool enabled) const
    {
        m_impl->SetPlaneDetectionEnabled(enabled);
    }

    bool System::Session::TrySetFeaturePointCloudEnabled(bool enabled) const
    {
        m_impl->FeaturePointCloudEnabled = enabled;
        return enabled;
    }

    bool System::Session::TrySetPreferredPlaneDetectorOptions(const GeometryDetectorOptions&)
    {
        // TODO
        return false;
    }

    bool System::Session::TrySetMeshDetectorEnabled(const bool enabled)
    {
        if (enabled) {
#if (__IPHONE_OS_VERSION_MAX_ALLOWED >= 130400)
        return m_impl->TrySetMeshDetectorEnabled(enabled);
#endif
        }
        return false;
    }

    bool System::Session::TrySetPreferredMeshDetectorOptions(const GeometryDetectorOptions&)
    {
        // TODO
        return false;
    }

    std::vector<ImageTrackingScore>* System::Session::GetImageTrackingScores() const
    {
        return m_impl->GetImageTrackingScores();
    }

    void System::Session::CreateAugmentedImageDatabase(const std::vector<System::Session::ImageTrackingRequest>& requests) const
    {
        m_impl->CreateAugmentedImageDatabase(requests);
    }
}
