DeclareModule TextEditor
	Declare Open(FileID.s, Path.s)
EndDeclareModule

Module TextEditor
	EnableExplicit

	#MaxEditors    = 16
	#ToolbarHeight = 35

	Global Dim _Windows.i(#MaxEditors)
	Global Dim _Editors.i(#MaxEditors)
	Global Dim _SaveBtns.i(#MaxEditors)
	Global Dim _FileIDs.s(#MaxEditors)
	Global Dim _Paths.s(#MaxEditors)
	Global Dim _Dirty.i(#MaxEditors)
	Global Dim _Saving.i(#MaxEditors)
	Global _Count.i       = 0
	Global _Loading_Slot.i = -1
	Global _ClosingSlot.i  = -1   ; slot awaiting confirm-close callback

	Declare _FindByWindow(Window.i)
	Declare _FindByEditor(Editor.i)
	Declare _SetDirty(Slot.i, Dirty.i)
	Declare _Save(Slot.i)
	Declare _Remove(Slot.i)
	Declare _ConfirmCloseCallback(Result.i)
	Declare _ReadCallback(Success.i, DataString.s)
	Declare _SaveCallback(Success.i, DataString.s)
	Declare Handler_Resize()
	Declare Handler_Save()
	Declare Handler_Change()
	Declare Handler_Close()

	Procedure Open(FileID.s, Path.s)
		If _Count >= #MaxEditors
			Notify::Toast("Too many editors open.", Notify::#Warning)
			ProcedureReturn
		EndIf

		Protected Slot.i = _Count
		Protected W.i    = 600
		Protected H.i    = 400
		Protected Win.i  = OpenWindow(#PB_Any, 120 + Slot * 20, 60 + Slot * 20, W, H,
		                              FS::GetFilePart(Path),
		                              #PB_Window_TitleBar | #PB_Window_SizeGadget | #PB_Window_SystemMenu)

		Protected SaveBtn.i = ButtonGadget(#PB_Any, 5, 3, 80, #ToolbarHeight - 6, "Save")
		Protected Editor.i  = EditorGadget(#PB_Any, 0, #ToolbarHeight, W, H - #ToolbarHeight)

		_Windows(Slot)  = Win
		_Editors(Slot)  = Editor
		_SaveBtns(Slot) = SaveBtn
		_FileIDs(Slot)  = FileID
		_Paths(Slot)    = Path
		_Dirty(Slot)    = #False
		_Count + 1

		BindGadgetEvent(SaveBtn, @Handler_Save())
		BindGadgetEvent(Editor,  @Handler_Change())
		BindEvent(#PB_Event_SizeWindow,  @Handler_Resize(), Win)
		BindEvent(#PB_Event_CloseWindow, @Handler_Close(),  Win)

		Desktop::Register(FS::GetFilePart(Path), Win, "📄")

		_Loading_Slot = Slot
		FS::Read_(FileID, Path, @_ReadCallback())
	EndProcedure

	; Private procedures
	Procedure.i _FindByWindow(Window.i)
		Protected i.i
		For i = 0 To _Count - 1
			If _Windows(i) = Window : ProcedureReturn i : EndIf
		Next
		ProcedureReturn -1
	EndProcedure

	Procedure.i _FindByEditor(Editor.i)
		Protected i.i
		For i = 0 To _Count - 1
			If _Editors(i) = Editor : ProcedureReturn i : EndIf
		Next
		ProcedureReturn -1
	EndProcedure

	Procedure _SetDirty(Slot.i, Dirty.i)
		_Dirty(Slot) = Dirty
		Protected Title.s = FS::GetFilePart(_Paths(Slot))
		If Dirty : Title = "* " + Title : EndIf
		SetWindowTitle(_Windows(Slot), Title)
	EndProcedure

	Procedure _Save(Slot.i)
		If Not IsWindow(_Windows(Slot)) : ProcedureReturn : EndIf
		If _Saving(Slot) : ProcedureReturn : EndIf
		Protected Content.s = GetGadgetText(_Editors(Slot))
		_Saving(Slot) = #True
		DisableGadget(_SaveBtns(Slot), #True)
		FS::Write(_FileIDs(Slot), _Paths(Slot), Content, @_SaveCallback())
	EndProcedure

	Procedure _Remove(Slot.i)
		UnbindGadgetEvent(_SaveBtns(Slot), @Handler_Save())
		UnbindGadgetEvent(_Editors(Slot),  @Handler_Change())
		UnbindEvent(#PB_Event_SizeWindow,  @Handler_Resize(), _Windows(Slot))
		UnbindEvent(#PB_Event_CloseWindow, @Handler_Close(),  _Windows(Slot))
		Desktop::Unregister(_Windows(Slot))

		Protected i.i
		For i = Slot To _Count - 2
			_Windows(i)  = _Windows(i + 1)
			_Editors(i)  = _Editors(i + 1)
			_SaveBtns(i) = _SaveBtns(i + 1)
			_FileIDs(i)  = _FileIDs(i + 1)
			_Paths(i)    = _Paths(i + 1)
			_Dirty(i)    = _Dirty(i + 1)
			_Saving(i)   = _Saving(i + 1)
		Next
		_Windows(_Count - 1)  = 0
		_Editors(_Count - 1)  = 0
		_SaveBtns(_Count - 1) = 0
		_FileIDs(_Count - 1)  = ""
		_Paths(_Count - 1)    = ""
		_Dirty(_Count - 1)    = #False
		_Saving(_Count - 1)   = #False
		_Count - 1

		If _Loading_Slot = Slot : _Loading_Slot = -1 : EndIf
		If _ClosingSlot  = Slot : _ClosingSlot  = -1 : EndIf
	EndProcedure

	; Called by Notify::Confirm — Result=1 means discard and close, 0 means cancel.
	Procedure _ConfirmCloseCallback(Result.i)
		If Result And _ClosingSlot >= 0
			_Remove(_ClosingSlot)
		EndIf
		_ClosingSlot = -1
	EndProcedure

	; Callbacks
	Procedure _ReadCallback(Success.i, DataString.s)
		If _Loading_Slot < 0 : ProcedureReturn : EndIf
		Protected Slot.i = _Loading_Slot
		_Loading_Slot = -1
		If Not IsWindow(_Windows(Slot)) : ProcedureReturn : EndIf
		If Success
			SetGadgetText(_Editors(Slot), DataString)
			_SetDirty(Slot, #False)
		Else
			SetGadgetText(_Editors(Slot), "")
			_SetDirty(Slot, #True)
		EndIf
	EndProcedure

	Procedure _SaveCallback(Success.i, DataString.s)
		Protected i.i
		For i = 0 To _Count - 1
			If _Saving(i)
				_Saving(i) = #False
				DisableGadget(_SaveBtns(i), #False)
				If Success
					_SetDirty(i, #False)
				Else
					Notify::Toast("Save failed — " + FS::GetFilePart(_Paths(i)), Notify::#Error)
				EndIf
				Break
			EndIf
		Next
	EndProcedure
	
	; Event handlers
	Procedure Handler_Resize()
		Protected Win.i  = EventWindow()
		Protected Slot.i = _FindByWindow(Win)
		If Slot < 0 : ProcedureReturn : EndIf
		ResizeGadget(_Editors(Slot), 0, #ToolbarHeight,
		             WindowWidth(Win), WindowHeight(Win) - #ToolbarHeight)
	EndProcedure

	Procedure Handler_Save()
		Protected Slot.i = _FindByWindow(EventWindow())
		If Slot >= 0 : _Save(Slot) : EndIf
	EndProcedure

	Procedure Handler_Change()
		Protected Slot.i = _FindByEditor(EventGadget())
		If Slot >= 0 And Not _Dirty(Slot)
			_SetDirty(Slot, #True)
		EndIf
	EndProcedure

	Procedure Handler_Close()
		Protected Win.i  = EventWindow()
		Protected Slot.i = _FindByWindow(Win)
		If Slot < 0 : ProcedureReturn : EndIf

		If _Dirty(Slot)
			_ClosingSlot = Slot
			Notify::Confirm("Unsaved changes",
			                "Close " + Chr(34) + FS::GetFilePart(_Paths(Slot)) + Chr(34) + " without saving?",
			                @_ConfirmCloseCallback())
		Else
			_Remove(Slot)
		EndIf
	EndProcedure

EndModule
; IDE Options = SpiderBasic 3.10 (Windows - x86)
; CursorPosition = 69
; Folding = DA5
; iOSAppOrientation = 0
; AndroidAppCode = 0
; AndroidAppOrientation = 0
; EnableXP
; DPIAware
; CompileSourceDirectory