DeclareModule FastCGI
	; Server
	Declare Open(Port, *Callback, BindedIP.s = "")                                          ; Create a FCGI Application on the given port. Return a Server object if succeed or 0 otherwise. Callback format : Callback(Request)
	Declare Close(*Server)																	; Close the given Server.
	
	; Request
	Declare FinishResponse(*Request)                                                        ; Send the response and close connection
	Declare.s GetCookie(*Request, Cookie.s)													; Return the value of the given cookie
	Declare.s GetParameter(*Request, Parameter.s)											; Return the value of the given parameter if it exists.
	Declare.s GetPostData(*Request)															; Return raw POST data
	Declare WriteResponseHeader(*Request, Header.s, Value.s)								; Write a header to the response
	Declare WriteResponseData(*Request, *Buffer, Length)									; Add data to the response
	Declare WriteResponseString(*Request, String.s, Format = #PB_UTF8)						; Add a string to the response
	Declare WriteResponseContentType(*Request, File.s)										; Write the MIME type based on file extension
	Declare RespondFile(*Request, File.s)													; Automatically send a file as a response
EndDeclareModule

Module FastCGI
	EnableExplicit
	
	Global NewMap MIMETypes.s()
	
	;{ Constants
	#FCGI_VERSION = 1
	
	; Record types
	#FCGI_BEGIN_REQUEST = 1
	#FCGI_ABORT_REQUEST = 2
	#FCGI_END_REQUEST = 3
	#FCGI_PARAMS = 4
	#FCGI_STDIN = 5
	#FCGI_STDOUT = 6
	#FCGI_STDERR = 7
	#FCGI_DATA = 8
	#FCGI_GET_VALUES = 9
	#FCGI_GET_VALUES_RESULT = 10
	#FCGI_UNKNOWN_TYPE = 11
	
	; Roles
	#FCGI_RESPONDER = 1
	#FCGI_AUTHORIZER = 2
	#FCGI_FILTER = 3
	
	; Flags
	#FCGI_KEEP_CONN = 1
	
	; Protocol status
	#FCGI_REQUEST_COMPLETE = 0
	#FCGI_CANT_MPX_CONN = 1
	#FCGI_OVERLOADED = 2
	#FCGI_UNKNOWN_ROLE = 3
	
	#HEADER_SIZE = 8
	#MAX_CONTENT_LENGTH = 65535
	#MAX_RECORD_SIZE = 65528  ; 65535 - 8 (aligned to 8 bytes)
	;}
	
	;{ Structures
	Structure FCGI_Header
		version.a
		type.a
		requestIdB1.a
		requestIdB0.a
		contentLengthB1.a
		contentLengthB0.a
		paddingLength.a
		reserved.a
	EndStructure
	
	Structure FCGI_BeginRequestBody
		roleB1.a
		roleB0.a
		flags.a
		reserved.a[5]
	EndStructure
	
	Structure FCGI_EndRequestBody
		appStatusB3.a
		appStatusB2.a
		appStatusB1.a
		appStatusB0.a
		protocolStatus.a
		reserved.a[3]
	EndStructure
	
	Structure Server
		ServerID.i
		Thread.i
		Stop.i
		*Callback
		Mutex.i
	EndStructure
	
	Structure Request
		ClientID.i
		RequestID.u
		KeepConnection.a
		Role.u
		ParamsComplete.a
		StdinComplete.a
		Map Parameters.s()
		Map ResponseHeaders.s()
		List Cookies.s()
		List ResponseData.i()
		PostData.s
		*PostDataBuffer
		PostDataLength.i
	EndStructure
	;}
	
	;{ Private procedure declarations
	Declare ServerThread(*Server)
	Declare ProcessNameValuePairs(*Data, *Request.Request, Length)
	Declare ReceiveAllData(ClientID, *Buffer, Length)
	Declare SendAllData(ClientID, *Buffer, Length)
	Declare BuildHeader(*Header.FCGI_Header, Type.a, RequestID.u, ContentLength.u, PaddingLength.a)
	Declare SendRecord(ClientID, Type.a, RequestID.u, *Content, ContentLength)
	Declare SendEndRequest(ClientID, RequestID.u, AppStatus.l, ProtocolStatus.a)
	;}
	
	;{ Helper procedures
	; Receive exactly 'Length' bytes, blocking until complete or error
	Procedure ReceiveAllData(ClientID, *Buffer, Length)
		Protected Received = 0, Result
		
		While Received < Length
			Result = ReceiveNetworkData(ClientID, *Buffer + Received, Length - Received)
			If Result <= 0
				ProcedureReturn -1  ; Connection closed or error
			EndIf
			Received + Result
		Wend
		
		ProcedureReturn Received
	EndProcedure
	
	; Send all data, blocking until complete
	Procedure SendAllData(ClientID, *Buffer, Length)
		Protected Sent = 0, Result
		
		While Sent < Length
			Result = SendNetworkData(ClientID, *Buffer + Sent, Length - Sent)
			If Result <= 0
				ProcedureReturn -1
			EndIf
			Sent + Result
		Wend
		
		ProcedureReturn Sent
	EndProcedure
	
	; Build a FastCGI header
	Procedure BuildHeader(*Header.FCGI_Header, Type.a, RequestID.u, ContentLength.u, PaddingLength.a)
		*Header\version = #FCGI_VERSION
		*Header\type = Type
		*Header\requestIdB1 = (RequestID >> 8) & $FF
		*Header\requestIdB0 = RequestID & $FF
		*Header\contentLengthB1 = (ContentLength >> 8) & $FF
		*Header\contentLengthB0 = ContentLength & $FF
		*Header\paddingLength = PaddingLength
		*Header\reserved = 0
	EndProcedure
	
	; Send a complete FastCGI record
	Procedure SendRecord(ClientID, Type.a, RequestID.u, *Content, ContentLength)
		Protected *Packet, PacketSize, PaddingLength.a, Result
		
		; Calculate padding to align to 8 bytes
		PaddingLength = (8 - (ContentLength % 8)) % 8
		PacketSize = #HEADER_SIZE + ContentLength + PaddingLength
		
		*Packet = AllocateMemory(PacketSize, #PB_Memory_NoClear)
		If Not *Packet
			ProcedureReturn #False
		EndIf
		
		; Build header
		BuildHeader(*Packet, Type, RequestID, ContentLength, PaddingLength)
		
		; Copy content if any
		If *Content And ContentLength > 0
			CopyMemory(*Content, *Packet + #HEADER_SIZE, ContentLength)
		EndIf
		
		; Zero padding
		If PaddingLength > 0
			FillMemory(*Packet + #HEADER_SIZE + ContentLength, PaddingLength, 0)
		EndIf
		
		; Send
		Result = SendAllData(ClientID, *Packet, PacketSize)
		FreeMemory(*Packet)
		
		ProcedureReturn Bool(Result > 0)
	EndProcedure
	
	; Send FCGI_END_REQUEST record
	Procedure SendEndRequest(ClientID, RequestID.u, AppStatus.l, ProtocolStatus.a)
		Protected Body.FCGI_EndRequestBody
		
		Body\appStatusB3 = (AppStatus >> 24) & $FF
		Body\appStatusB2 = (AppStatus >> 16) & $FF
		Body\appStatusB1 = (AppStatus >> 8) & $FF
		Body\appStatusB0 = AppStatus & $FF
		Body\protocolStatus = ProtocolStatus
		
		ProcedureReturn SendRecord(ClientID, #FCGI_END_REQUEST, RequestID, @Body, SizeOf(FCGI_EndRequestBody))
	EndProcedure
	
	; Parse name-value pairs from FCGI_PARAMS
	Procedure ProcessNameValuePairs(*Data, *Request.Request, Length)
		Protected Offset = 0
		Protected NameLength.l, ValueLength.l
		Protected Name.s, Value.s
		
		While Offset < Length
			; Read name length
			NameLength = PeekA(*Data + Offset)
			If NameLength & $80  ; High bit set = 4-byte length
				If Offset + 4 > Length : Break : EndIf
				NameLength = ((PeekA(*Data + Offset) & $7F) << 24) |
				             (PeekA(*Data + Offset + 1) << 16) |
				             (PeekA(*Data + Offset + 2) << 8) |
				             PeekA(*Data + Offset + 3)
				Offset + 4
			Else
				Offset + 1
			EndIf
			
			; Read value length
			If Offset >= Length : Break : EndIf
			ValueLength = PeekA(*Data + Offset)
			If ValueLength & $80  ; High bit set = 4-byte length
				If Offset + 4 > Length : Break : EndIf
				ValueLength = ((PeekA(*Data + Offset) & $7F) << 24) |
				              (PeekA(*Data + Offset + 1) << 16) |
				              (PeekA(*Data + Offset + 2) << 8) |
				              PeekA(*Data + Offset + 3)
				Offset + 4
			Else
				Offset + 1
			EndIf
			
			; Bounds check
			If Offset + NameLength + ValueLength > Length
				Break
			EndIf
			
			; Read name and value
			If NameLength > 0
				Name = PeekS(*Data + Offset, NameLength, #PB_Ascii)
				Offset + NameLength
				
				If ValueLength > 0
					Value = PeekS(*Data + Offset, ValueLength, #PB_Ascii)
					Offset + ValueLength
				Else
					Value = ""
				EndIf
				
				*Request\Parameters(Name) = Value
			Else
				Offset + ValueLength
			EndIf
		Wend
	EndProcedure
	;}
	
	;{ Public procedures - Server
	Procedure Close(*Server.Server)
		If *Server
			*Server\Stop = #True
			; Wait for thread to finish
			If IsThread(*Server\Thread)
				WaitThread(*Server\Thread, 5000)
				If IsThread(*Server\Thread)
					KillThread(*Server\Thread)
				EndIf
			EndIf
			If *Server\Mutex
				FreeMutex(*Server\Mutex)
			EndIf
			FreeMemory(*Server)
		EndIf
	EndProcedure
	
	Procedure Open(Port, *Callback, BindedIP.s = "")
		Protected ServerID, *Server.Server
		
		If Not *Callback
			ProcedureReturn #Null
		EndIf
		
		ServerID = CreateNetworkServer(#PB_Any, Port, #PB_Network_TCP, BindedIP)
		
		If ServerID
			*Server = AllocateMemory(SizeOf(Server))
			If *Server
				*Server\ServerID = ServerID
				*Server\Callback = *Callback
				*Server\Mutex = CreateMutex()
				*Server\Thread = CreateThread(@ServerThread(), *Server)
				
				If Not *Server\Thread
					CloseNetworkServer(ServerID)
					If *Server\Mutex : FreeMutex(*Server\Mutex) : EndIf
					FreeMemory(*Server)
					*Server = #Null
				EndIf
			Else
				CloseNetworkServer(ServerID)
			EndIf
		EndIf
		
		ProcedureReturn *Server
	EndProcedure
	;}
	
	;{ Public procedures - Request
	Procedure FinishResponse(*Request.Request)
		Protected HeaderString.s, *HeaderData, HeaderLength
		Protected *ContentData, ContentLength, TotalLength
		Protected Offset, ChunkSize, Result
		
		If Not *Request
			ProcedureReturn
		EndIf
		
		; Build HTTP headers string
		ForEach *Request\ResponseHeaders()
			HeaderString + MapKey(*Request\ResponseHeaders()) + ": " + *Request\ResponseHeaders() + #CRLF$
		Next
		
		ForEach *Request\Cookies()
			HeaderString + "Set-Cookie: " + *Request\Cookies() + #CRLF$
		Next
		
		HeaderString + #CRLF$  ; End of headers
		
		; Calculate total response size
		HeaderLength = StringByteLength(HeaderString, #PB_Ascii)
		ContentLength = 0
		ForEach *Request\ResponseData()
			ContentLength + MemorySize(*Request\ResponseData())
		Next
		TotalLength = HeaderLength + ContentLength
		
		; Allocate combined buffer
		*ContentData = AllocateMemory(TotalLength, #PB_Memory_NoClear)
		If Not *ContentData
			ProcedureReturn
		EndIf
		
		; Copy headers
		PokeS(*ContentData, HeaderString, -1, #PB_Ascii | #PB_String_NoZero)
		Offset = HeaderLength
		
		; Copy response data
		ForEach *Request\ResponseData()
			CopyMemory(*Request\ResponseData(), *ContentData + Offset, MemorySize(*Request\ResponseData()))
			Offset + MemorySize(*Request\ResponseData())
			FreeMemory(*Request\ResponseData())
		Next
		ClearList(*Request\ResponseData())
		
		; Send STDOUT records (split if necessary)
		Offset = 0
		While Offset < TotalLength
			ChunkSize = TotalLength - Offset
			If ChunkSize > #MAX_RECORD_SIZE
				ChunkSize = #MAX_RECORD_SIZE
			EndIf
			
			Result = SendRecord(*Request\ClientID, #FCGI_STDOUT, *Request\RequestID, *ContentData + Offset, ChunkSize)
			If Not Result
				Break
			EndIf
			Offset + ChunkSize
		Wend
		
		FreeMemory(*ContentData)
		
		; Send empty STDOUT to signal end of output
		SendRecord(*Request\ClientID, #FCGI_STDOUT, *Request\RequestID, #Null, 0)
		
		; Send END_REQUEST
		SendEndRequest(*Request\ClientID, *Request\RequestID, 0, #FCGI_REQUEST_COMPLETE)
		
		; Close connection if not keep-alive
		If Not *Request\KeepConnection
			CloseNetworkConnection(*Request\ClientID)
		EndIf
		
		; Clean up POST data buffer if allocated
		If *Request\PostDataBuffer
			FreeMemory(*Request\PostDataBuffer)
			*Request\PostDataBuffer = #Null
		EndIf
	EndProcedure
	
	Procedure.s GetCookie(*Request.Request, Cookie.s)
		Protected CookieHeader.s, Result.s
		Protected SearchStr.s, StartPos, EndPos
		
		If Not *Request
			ProcedureReturn ""
		EndIf
		
		CookieHeader = *Request\Parameters("HTTP_COOKIE")
		If CookieHeader = ""
			ProcedureReturn ""
		EndIf
		
		SearchStr = Cookie + "="
		StartPos = FindString(CookieHeader, SearchStr)
		
		If StartPos
			StartPos + Len(SearchStr)
			EndPos = FindString(CookieHeader, ";", StartPos)
			
			If EndPos = 0
				EndPos = Len(CookieHeader) + 1
			EndIf
			
			Result = Trim(Mid(CookieHeader, StartPos, EndPos - StartPos))
		EndIf
		
		ProcedureReturn Result
	EndProcedure
	
	Procedure.s GetParameter(*Request.Request, Parameter.s)
		If *Request
			ProcedureReturn *Request\Parameters(Parameter)
		EndIf
		ProcedureReturn ""
	EndProcedure
	
	Procedure.s GetPostData(*Request.Request)
		If *Request
			ProcedureReturn *Request\PostData
		EndIf
		ProcedureReturn ""
	EndProcedure
	
	Procedure WriteResponseHeader(*Request.Request, Header.s, Value.s)
		If *Request
			If LCase(Header) = "set-cookie" Or Header = #PB_CGI_HeaderSetCookie
				AddElement(*Request\Cookies())
				*Request\Cookies() = Value
			Else
				*Request\ResponseHeaders(Header) = Value
			EndIf
		EndIf
	EndProcedure
	
	Procedure WriteResponseString(*Request.Request, String.s, Format = #PB_UTF8)
		Protected *Buffer, Length
		
		If *Request And String <> ""
			Length = StringByteLength(String, Format)
			*Buffer = AllocateMemory(Length, #PB_Memory_NoClear)
			If *Buffer
				PokeS(*Buffer, String, -1, Format | #PB_String_NoZero)
				AddElement(*Request\ResponseData())
				*Request\ResponseData() = *Buffer
			EndIf
		EndIf
	EndProcedure
	
	Procedure WriteResponseData(*Request.Request, *Buffer, Length)
		Protected *Copy
		
		If *Request And *Buffer And Length > 0
			*Copy = AllocateMemory(Length, #PB_Memory_NoClear)
			If *Copy
				CopyMemory(*Buffer, *Copy, Length)
				AddElement(*Request\ResponseData())
				*Request\ResponseData() = *Copy
			EndIf
		EndIf
	EndProcedure
	
	Procedure WriteResponseContentType(*Request.Request, File.s)
		Protected Extension.s, MIMEType.s
		
		If *Request
			Extension = LCase(GetExtensionPart(File))
			If FindMapElement(MIMETypes(), Extension)
				MIMEType = MIMETypes()
			Else
				MIMEType = "application/octet-stream"
			EndIf
			*Request\ResponseHeaders("Content-Type") = MIMEType
		EndIf
	EndProcedure
	
	Procedure RespondFile(*Request.Request, File.s)
		Protected Result = #False
		Protected FileID, FileSize, *FileData
		
		If Not *Request
			ProcedureReturn #False
		EndIf
		
		FileID = ReadFile(#PB_Any, File, #PB_File_SharedRead)
		If FileID
			FileSize = Lof(FileID)
			If FileSize > 0
				*FileData = AllocateMemory(FileSize, #PB_Memory_NoClear)
				If *FileData
					ReadData(FileID, *FileData, FileSize)
					WriteResponseContentType(*Request, File)
					WriteResponseData(*Request, *FileData, FileSize)
					FreeMemory(*FileData)
					Result = #True
				EndIf
			EndIf
			CloseFile(FileID)
			
			If Result
				FinishResponse(*Request)
			EndIf
		EndIf
		
		ProcedureReturn Result
	EndProcedure
	;}
	
	;{ Server thread
	Procedure ServerThread(*Server.Server)
		Protected Header.FCGI_Header
		Protected BeginBody.FCGI_BeginRequestBody
		Protected ContentLength, PaddingLength
		Protected *ContentBuffer, *PaddingBuffer
		Protected Event, ClientID
		Protected NewMap Requests.Request()
		Protected *Request.Request
		Protected RequestKey.s
		
		*ContentBuffer = AllocateMemory(#MAX_CONTENT_LENGTH)
		*PaddingBuffer = AllocateMemory(256)
		
		If Not *ContentBuffer Or Not *PaddingBuffer
			If *ContentBuffer : FreeMemory(*ContentBuffer) : EndIf
			If *PaddingBuffer : FreeMemory(*PaddingBuffer) : EndIf
			ProcedureReturn
		EndIf
		
		Repeat
			Event = NetworkServerEvent(*Server\ServerID)
			
			Select Event
				Case #PB_NetworkEvent_None
					Delay(1)
					
				Case #PB_NetworkEvent_Connect
					ClientID = EventClient()
					; New connection - request will be created on BEGIN_REQUEST
					
				Case #PB_NetworkEvent_Data
					ClientID = EventClient()
					RequestKey = Str(ClientID)
					
					; Read header
					If ReceiveAllData(ClientID, @Header, #HEADER_SIZE) = #HEADER_SIZE
						ContentLength = (Header\contentLengthB1 << 8) | Header\contentLengthB0
						PaddingLength = Header\paddingLength
						
						; Read content
						If ContentLength > 0
							If ReceiveAllData(ClientID, *ContentBuffer, ContentLength) <> ContentLength
								Continue
							EndIf
						EndIf
						
						; Read padding
						If PaddingLength > 0
							ReceiveAllData(ClientID, *PaddingBuffer, PaddingLength)
						EndIf
						
						; Process based on record type
						Select Header\type
							Case #FCGI_BEGIN_REQUEST
								; Create new request
								*Request = AddMapElement(Requests(), RequestKey)
								If *Request
									*Request\ClientID = ClientID
									*Request\RequestID = (Header\requestIdB1 << 8) | Header\requestIdB0
									
									If ContentLength >= SizeOf(FCGI_BeginRequestBody)
										CopyMemory(*ContentBuffer, @BeginBody, SizeOf(FCGI_BeginRequestBody))
										*Request\Role = (BeginBody\roleB1 << 8) | BeginBody\roleB0
										*Request\KeepConnection = Bool(BeginBody\flags & #FCGI_KEEP_CONN)
									EndIf
								EndIf
								
							Case #FCGI_PARAMS
								*Request = FindMapElement(Requests(), RequestKey)
								If *Request
									If ContentLength > 0
										ProcessNameValuePairs(*ContentBuffer, *Request, ContentLength)
									Else
										; Empty PARAMS record signals end of parameters
										*Request\ParamsComplete = #True
									EndIf
								EndIf
								
							Case #FCGI_STDIN
								*Request = FindMapElement(Requests(), RequestKey)
								If *Request
									If ContentLength > 0
										; Accumulate POST data
										Protected *NewBuffer, NewLength
										NewLength = *Request\PostDataLength + ContentLength
										*NewBuffer = AllocateMemory(NewLength)
										If *NewBuffer
											If *Request\PostDataBuffer
												CopyMemory(*Request\PostDataBuffer, *NewBuffer, *Request\PostDataLength)
												FreeMemory(*Request\PostDataBuffer)
											EndIf
											CopyMemory(*ContentBuffer, *NewBuffer + *Request\PostDataLength, ContentLength)
											*Request\PostDataBuffer = *NewBuffer
											*Request\PostDataLength = NewLength
										EndIf
									Else
										; Empty STDIN record signals end of input
										*Request\StdinComplete = #True
										
										; Convert POST data to string
										If *Request\PostDataBuffer And *Request\PostDataLength > 0
											*Request\PostData = PeekS(*Request\PostDataBuffer, *Request\PostDataLength, #PB_UTF8 | #PB_ByteLength)
										EndIf
										
										; Request is complete - call handler
										If *Request\ParamsComplete
											Protected Callback.i = *Server\Callback
											If Callback
												CallFunctionFast(Callback, *Request)
											EndIf
											
											; Clean up request from map after response
											DeleteMapElement(Requests(), RequestKey)
										EndIf
									EndIf
								EndIf
								
							Case #FCGI_ABORT_REQUEST
								*Request = FindMapElement(Requests(), RequestKey)
								If *Request
									If *Request\PostDataBuffer
										FreeMemory(*Request\PostDataBuffer)
									EndIf
									ForEach *Request\ResponseData()
										FreeMemory(*Request\ResponseData())
									Next
									DeleteMapElement(Requests(), RequestKey)
								EndIf
								
						EndSelect
					EndIf
					
				Case #PB_NetworkEvent_Disconnect
					ClientID = EventClient()
					RequestKey = Str(ClientID)
					
					; Clean up any pending request
					*Request = FindMapElement(Requests(), RequestKey)
					If *Request
						If *Request\PostDataBuffer
							FreeMemory(*Request\PostDataBuffer)
						EndIf
						ForEach *Request\ResponseData()
							FreeMemory(*Request\ResponseData())
						Next
						DeleteMapElement(Requests(), RequestKey)
					EndIf
					
			EndSelect
			
		Until *Server\Stop
		
		; Cleanup
		FreeMemory(*ContentBuffer)
		FreeMemory(*PaddingBuffer)
		
		; Clean up remaining requests
		ForEach Requests()
			If Requests()\PostDataBuffer
				FreeMemory(Requests()\PostDataBuffer)
			EndIf
			ForEach Requests()\ResponseData()
				FreeMemory(Requests()\ResponseData())
			Next
		Next
		
		CloseNetworkServer(*Server\ServerID)
	EndProcedure
	;}
	
	;{ MIME Types
	Procedure LoadMimeTypes()
		Protected.s Ext, Type
		Restore MimeData
		Read.s Ext
		While Ext <> "END"
			Read.s Type
			MIMETypes(Ext) = Type
			Read.s Ext
		Wend
	EndProcedure
	
	LoadMimeTypes()
	
	DataSection
		MimeData:
		Data.s "aac", "audio/aac", "abw", "application/x-abiword", "apng", "image/apng", "avi", "video/x-msvideo", "bin", "application/octet-stream", "bmp", "image/bmp"
		Data.s "css", "text/css", "csv", "text/csv", "doc", "application/msword", "gif", "image/gif", "htm", "text/html", "html", "text/html", "ico", "image/x-icon"
		Data.s "jpeg", "image/jpeg", "jpg", "image/jpeg", "js", "text/javascript", "json", "application/json", "mp3", "audio/mpeg", "mp4", "video/mp4", "mpeg", "video/mpeg"
		Data.s "otf", "font/otf", "png", "image/png", "pdf", "application/pdf", "php", "application/x-httpd-php", "svg", "image/svg+xml", "txt", "text/plain"
		Data.s "wav", "audio/wav", "webm", "video/webm", "webp", "image/webp", "woff", "font/woff", "woff2", "font/woff2", "xml", "application/xml", "zip", "application/zip"
		Data.s "END"
	EndDataSection
	;}
EndModule

;{ Test/Demo code
CompilerIf #PB_Compiler_IsMainFile
	
	Global *ImageData
	Global ImageSize
	
	; Load test image
	If ReadFile(0, #PB_Compiler_Home + "examples/sources/Data/Map.bmp")
		ImageSize = Lof(0)
		*ImageData = AllocateMemory(ImageSize)
		ReadData(0, *ImageData, ImageSize)
		CloseFile(0)
	Else
		; Create a simple test response if image not found
		*ImageData = #Null
		ImageSize = 0
	EndIf
	
	Procedure RequestHandler(*Request)
		Protected URI.s
		
		URI = FastCGI::GetParameter(*Request, "REQUEST_URI")
		
		Debug "Request received: " + URI
		Debug "Method: " + FastCGI::GetParameter(*Request, "REQUEST_METHOD")
		Debug "Query: " + FastCGI::GetParameter(*Request, "QUERY_STRING")
		
		If *ImageData And ImageSize > 0
			FastCGI::WriteResponseContentType(*Request, "test.bmp")
			FastCGI::WriteResponseData(*Request, *ImageData, ImageSize)
		Else
			FastCGI::WriteResponseHeader(*Request, "Content-Type", "text/html")
			FastCGI::WriteResponseString(*Request, "<html><body><h1>FastCGI Test</h1><p>Request URI: " + URI + "</p></body></html>")
		EndIf
		
		FastCGI::FinishResponse(*Request)
	EndProcedure
	
	OpenConsole("FastCGI Demo")
	PrintN("Starting FastCGI server on port 5600...")
	PrintN("Press Enter to stop")
	
	Global Server = FastCGI::Open(5600, @RequestHandler())
	
	If Server
		PrintN("Server started successfully")
		Input()
		FastCGI::Close(Server)
		PrintN("Server closed")
	Else
		PrintN("Failed to start server!")
	EndIf
	
	If *ImageData
		FreeMemory(*ImageData)
	EndIf
	
	PrintN("Press Enter to exit")
	Input()
CompilerEndIf
;}

; IDE Options = PureBasic 6.30 (Windows - x64)
; ExecutableFormat = Console
; CursorPosition = 55
; Folding = GAAAA9
; EnableXP
; DPIAware