/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */

#import "OpenVulkanoView.h"

#include "Base/Logger.hpp"
#include "Input/Touch/InputDeviceTouch.hpp"
#include "Input/InputManager.hpp"
#include "Host/Apple/MetalViewWindow.h"
#include <map>

#import <MetalKit/MTKView.h>

using namespace OpenVulkano;

#pragma mark MetalViewDelegate
@interface MetalViewDelegate : NSObject<MTKViewDelegate>
- (void) mtkView:(MTKView *) view drawableSizeWillChange:(CGSize) size;
- (void) drawInMTKView:(MTKView *) view;
@end

@implementation MetalViewDelegate
{
	MetalViewWindow* window;
}

- (id)initWithWindow:(MetalViewWindow*)win
{
	window = win;
	return self;
}

- (void) mtkView:(MTKView *) view drawableSizeWillChange:(CGSize) size
{
	window->SetContentScale(UIScreen.mainScreen.nativeScale);
	window->OnResize(size.width, size.height);
}

- (void) drawInMTKView:(MTKView *) view
{
	@autoreleasepool {
		window->TickHandler();
	}
}
@end

#pragma mark -
#pragma mark OpenVulkanoView

@interface OpenVulkanoView()
{
	CADisplayLink* m_displayLink;
	MetalViewDelegate* m_mtdelegate;
	std::map<UITouch*, int32_t> m_touchToIdMap;
	Input::InputDeviceTouch m_touchDevice;
	MetalViewWindow m_window;
}
@end

#pragma mark OpenVulkanoView Implementation
@implementation OpenVulkanoView
/** Returns a Metal-compatible layer. */
+(Class) layerClass { return [CAMetalLayer class]; }

- (id)initWithFrame:(CGRect)frame {
	self = [super initWithFrame:frame];
	if (self) {
		[self commonInit];
	}
	return self;
}

- (id)initWithCoder:(NSCoder *)aDecoder {
	self = [super initWithCoder:aDecoder];
	if (self) {
		[self commonInit];
	}
	return self;
}

- (void)commonInit {
	[self setMultipleTouchEnabled:YES];
	
	self.contentScaleFactor = UIScreen.mainScreen.nativeScale;
	auto size = self.bounds.size;
	auto sizeX = size.width * self.contentScaleFactor;
	auto sizeY = size.height * self.contentScaleFactor;
	m_window.Init(self.layer, {sizeX, sizeY});

	m_touchDevice.Init(); //TODO link window
	Input::InputManager::GetInstance()->RegisterInputDevice(&m_touchDevice);

	if ([self isKindOfClass:[MTKView class]]) {
		MTKView* mtkView = (MTKView*)(self);
		m_mtdelegate = [[MetalViewDelegate alloc] initWithWindow:&m_window];
		[mtkView setDelegate:m_mtdelegate];
		mtkView.preferredFramesPerSecond = 60;
	} else {
		m_displayLink = [CADisplayLink displayLinkWithTarget: self selector: @selector(renderLoop)];
		[m_displayLink setPreferredFramesPerSecond: 60];
		[m_displayLink addToRunLoop: NSRunLoop.currentRunLoop forMode: NSDefaultRunLoopMode];
		m_displayLink.paused = YES;
	}
}

- (void)dealloc {
	for(auto& it : m_touchToIdMap) {
		m_touchDevice.RemoveTouch(it.second);
	}
	Input::InputManager::GetInstance()->UnregisterInputDevice(&m_touchDevice);
	m_touchDevice.Close();
	[m_displayLink release];
	[m_mtdelegate release];
	OpenVulkano::Logger::WINDOW->info("Destroyed OpenVulkanoView");
	[super dealloc];
}

- (void)layoutSubviews {
	[super layoutSubviews];

	if (m_displayLink)
	{ // Only use this update method if we use display link instead of metal delegate
		auto size = self.bounds.size;
		auto sizeX = size.width * self.contentScaleFactor;
		auto sizeY = size.height * self.contentScaleFactor;
		m_window.SetContentScale(self.contentScaleFactor);
		m_window.OnResize(sizeX, sizeY);
	}
}


- (void)SetInterfaceOrientation:(float)orientation
{
	m_window.SetOrientation(orientation);
}

- (Math::Vector2f)getTouchPosition:(UITouch*)touch
{
	CGPoint uitouchLocation = [touch locationInView:touch.view];
	return { uitouchLocation.x * self.contentScaleFactor, uitouchLocation.y * self.contentScaleFactor };
}

- (void)touchesBegan:(NSSet*)inTouches withEvent:(UIEvent*)inEvent
{
	for (UITouch* touch in inTouches)
	{
		Math::Vector2f touchLocation = [self getTouchPosition: touch];
		uint32_t touchId = m_touchDevice.AddTouch(touchLocation);
		m_touchDevice.TouchDown(touchId);
		m_touchToIdMap.insert(std::make_pair(touch, touchId));
	}
}

- (void)touchesCancelled:(NSSet*)inTouches withEvent:(UIEvent*)inEvent
{
	for (UITouch* touch in inTouches)
	{
		Math::Vector2f touchLocation = [self getTouchPosition: touch];

		auto it = m_touchToIdMap.find(touch);
		if (it != m_touchToIdMap.end())
		{
			m_touchDevice.TouchUp(it->second);
			m_touchDevice.RemoveTouch(it->second);
			m_touchToIdMap.erase(it);
		}
	}
}

- (void)touchesEnded:(NSSet*)inTouches withEvent:(UIEvent*)inEvent
{
	for (UITouch* touch in inTouches)
	{
		Math::Vector2f touchLocation = [self getTouchPosition: touch];

		auto it = m_touchToIdMap.find(touch);
		if (it != m_touchToIdMap.end())
		{
			m_touchDevice.TouchUp(it->second);
			m_touchDevice.RemoveTouch(it->second);
			m_touchToIdMap.erase(it);
		}
	}
}

- (void)touchesMoved:(NSSet*)inTouches withEvent:(UIEvent*)inEvent
{
	for (UITouch* touch in inTouches)
	{
		Math::Vector2f touchLocation = [self getTouchPosition: touch];

		auto it = m_touchToIdMap.find(touch);
		if (it != m_touchToIdMap.end())
		{
			m_touchDevice.TouchMoved(it->second, touchLocation);
		}
	}
}

-(void*)GetWindow
{
	return &m_window;
}

-(void) renderLoop
{
	m_window.TickHandler();
}

-(void)WillAppear
{
	if (m_displayLink) m_displayLink.paused = NO;
}

-(void)DidAppear
{
	if (m_displayLink) m_displayLink.paused = NO;
}

-(void)WillDisappear
{
	if (m_displayLink) m_displayLink.paused = YES;
}

-(void)DidDisappear
{
	if (m_displayLink) m_displayLink.paused = YES;
}

-(void)DidUnload
{
	if (m_displayLink)
	{
		[m_displayLink invalidate];
		[m_displayLink release];
		m_displayLink = nil;
	}
}
@end

