import Foundation
import SDL


public protocol ApplicationImplementation
{
	func create(render: inout Renderer)
	func quit()
	func loadContent(content: inout ContentManager) throws
	func resize(width: Int, height: Int)
	func update(deltaTime: Float)
	func draw(render: inout Renderer, deltaTime: Float)
}

extension ApplicationImplementation
{
	public func quit() {}
}

public struct ApplicationConfiguration
{
	let resizable: Bool
	let vSync: VSyncMode
	let windowWidth: Int32
	let windowHeight: Int32
	let windowTitle: String
	let bundle: Bundle

	public init(resizable: Bool, vSync: VSyncMode, windowWidth: Int32, windowHeight: Int32, title: String, bundle: Bundle)
	{
		self.resizable = resizable
		self.vSync = vSync
		self.windowWidth = windowWidth
		self.windowHeight = windowHeight
		self.windowTitle = title
		self.bundle = bundle
	}
}


public class Application
{
	private let implementation: ApplicationImplementation
	private let configuration: ApplicationConfiguration

	private var window: OpaquePointer?
	private var render: Renderer
	private var content: ContentManager

	typealias ResizeEvent = (width: Int32, height: Int32)
	private var resize: ResizeEvent? = nil

	public init(application: ApplicationImplementation, config: ApplicationConfiguration)
	{
		self.implementation = application
		self.configuration = config
		self.render = OpenGL(version: .init(major: 1, minor: 5))
		self.content = ContentManager(&self.render, bundle: configuration.bundle)
	}

	public func run()
	{
		guard SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) == 0
		else { fatalError("SDL_Init: \(String(cString: SDL_GetError()))") }

		let winPos = Int32(SDL_WINDOWPOS_UNDEFINED_MASK)
		let winWidth: Int32 = configuration.windowWidth, winHeight: Int32 = configuration.windowHeight
		var winFlags = SDL_WINDOW_RESIZABLE.rawValue | SDL_WINDOW_ALLOW_HIGHDPI.rawValue
		if render is OpenGL { winFlags |= SDL_WINDOW_OPENGL.rawValue }
		window = SDL_CreateWindow(configuration.windowTitle, winPos, winPos, winWidth, winHeight, winFlags)
		guard window != nil else { fatalError("SDL_CreateWindow: \(String(cString: SDL_GetError()))") }

		do
		{
			try render.create(sdlWindow: window!)
			try render.setVsync(mode: configuration.vSync)
		}
		catch RendererError.sdlError(let message)
		{
			fatalError("idk error \(message)")
		}
		catch { fatalError("piss") }

		for idx in 0...SDL_NumJoysticks()
		{
			if SDL_IsGameController(idx) != SDL_TRUE { continue }
			Input.instance.padConnectedEvent(index: idx)
		}

		do
		{
			implementation.create(render: &render)

			content.setLoader(extension: "obj", loader: ObjLoader())
			content.setLoader(extension: "g3db", loader: G3DbLoader())
			content.setLoader(extensions: ["png", "jpg"], loader: NSImageLoader())
			try implementation.loadContent(content: &content)
		}
		catch
		{
			fatalError("loadContent()")
		}

		let timeMul = 1 / Double(SDL_GetPerformanceFrequency());
		var prevTime = SDL_GetPerformanceCounter();

		resize = displaySize

		mainLoop: while true
		{
			Input.instance.newTick()

			var event = SDL_Event()
			while SDL_PollEvent(&event) > 0
			{
				switch SDL_EventType(event.type)
				{
				case SDL_QUIT: break mainLoop
				case SDL_WINDOWEVENT:
					switch SDL_WindowEventID(UInt32(event.window.event))
					{
					case SDL_WINDOWEVENT_RESIZED:
						resize = displaySize
					default: break
					}
				case SDL_KEYDOWN:
					Input.instance.pressEvent(scan: event.key.keysym.scancode, repeat: event.key.repeat != 0)
				case SDL_KEYUP:
					Input.instance.releaseEvent(scan: event.key.keysym.scancode)
				case SDL_CONTROLLERDEVICEADDED:
					if SDL_IsGameController(event.cdevice.which) != SDL_TRUE { break }
					Input.instance.padConnectedEvent(index: event.cdevice.which)
				case SDL_CONTROLLERDEVICEREMOVED:
					Input.instance.padRemovedEvent(index: event.cdevice.which)
				case SDL_CONTROLLERBUTTONDOWN:
					Input.instance.padButtonPressEvent(id: event.cbutton.which, btn: event.cbutton.button)
				case SDL_CONTROLLERBUTTONUP:
					Input.instance.padButtonReleaseEvent(id: event.cbutton.which, btn: event.cbutton.button)
				case SDL_CONTROLLERAXISMOTION:
					Input.instance.padAxisEvent(id: event.caxis.which, axis: event.caxis.axis, value: event.caxis.value)
				default: break
				}
			}

			let time = SDL_GetPerformanceCounter();
			let deltaTime = Float(Double(time &- prevTime) * timeMul)
			prevTime = time

			implementation.update(deltaTime: min(deltaTime, 1.0 / 15.0))
			repaint(deltaTime)
		}

		implementation.quit()

		content.releaseAll()
		render.delete()
		SDL_DestroyWindow(window)
		SDL_Quit()
	}

	private func repaint(_ deltaTime: Float)
	{
		if resize != nil
		{
			render.resize(width: resize!.width, height: resize!.height)
			implementation.resize(width: Int(resize!.width), height: Int(resize!.height))
			resize = nil
		}

		render.newFrame()
		implementation.draw(render: &render, deltaTime: deltaTime)

		SDL_GL_SwapWindow(window)
	}

	private var displaySize: ResizeEvent
	{
		var width: Int32 = 0, height: Int32 = 0
		SDL_GL_GetDrawableSize(window, &width, &height)
		return (width, height)
	}
}
