DeclareModule FileExplorer
	Declare Open()
EndDeclareModule

Module FileExplorer
	EnableExplicit

	; Layout constants
	#Toolbar_Height  = 35
	#Statusbar_Height = 24
	#Window_W = 480
	#Window_H = 400
	#Max_Entries = 512

	; Gadgets
	Global Window
	Global UpButton
	Global PathLabel
	Global NewFolderButton
	Global NewFileButton
	Global ListView
	Global StatusLabel

	; Navigation state
	Global CurrentPath.s = "/"
	Global Dim PathStack.s(64)
	Global StackDepth = 0
	
	; Entry cache (populated on each listing)
	Global Dim EntryIDs.s(#Max_Entries)
	Global Dim EntryNames.s(#Max_Entries)
	Global Dim EntryIsDir(#Max_Entries)
	Global EntryCount = 0

	; Input window state
	Global InputWindow   = 0
	Global InputField
	Global InputMode     ; 0 = new folder, 1 = new file
	Global ButtonNew
	Global CancelBtn

	; Private declarations
	Declare _Load(Path.s)
	Declare _ListCallback(Success.i, DataString.s)
	Declare _MkdirCallback(Success.i, DataString.s)
	Declare _CreateFileCallback(Success.i, DataString.s)
	Declare _OpenInputWindow(Mode.i)
	Declare _CloseInputWindow()
	Declare Handler_Resize()
	Declare Handler_Up()
	Declare Handler_NewFolder()
	Declare Handler_NewFile()
	Declare Handler_ListView()
	Declare Handler_Close()
	Declare Handler_InputConfirm()
	Declare Handler_InputCancel()

	; Public procedures

	Procedure Open()
		If IsWindow(Window)
			SetActiveWindow(Window)
			ProcedureReturn
		EndIf

		Window = OpenWindow(#PB_Any, 150, 80, #Window_W, #Window_H, "File Explorer",
		                     #PB_Window_TitleBar | #PB_Window_SizeGadget | #PB_Window_SystemMenu)

		; Toolbar
		UpButton       = ButtonGadget(#PB_Any,  2,  4,  30, #Toolbar_Height - 8, "↑")
		PathLabel      = TextGadget  (#PB_Any, 36,  4, #Window_W - 200, #Toolbar_Height - 8, "/")
		NewFolderButton= ButtonGadget(#PB_Any, #Window_W - 162, 4, 80, #Toolbar_Height - 8, "+ Folder")
		NewFileButton  = ButtonGadget(#PB_Any, #Window_W - 78,  4, 74, #Toolbar_Height - 8, "+ File")

		; File list
		ListView = ListViewGadget(#PB_Any, 0, #Toolbar_Height,
		                           #Window_W, #Window_H - #Toolbar_Height - #Statusbar_Height)

		; Status bar
		StatusLabel = TextGadget(#PB_Any, 4, #Window_H - #Statusbar_Height + 4,
		                          #Window_W - 8, #Statusbar_Height - 8, "")

		BindGadgetEvent(UpButton,        @Handler_Up())
		BindGadgetEvent(NewFolderButton, @Handler_NewFolder())
		BindGadgetEvent(NewFileButton,   @Handler_NewFile())
		BindGadgetEvent(ListView,        @Handler_ListView())
		BindEvent(#PB_Event_SizeWindow,   @Handler_Resize(), Window)
		BindEvent(#PB_Event_CloseWindow,  @Handler_Close(),  Window)

		Desktop::Register("File Explorer", Window, "📁")

		CurrentPath  = "/"
		StackDepth   = 0
		_Load("/")
	EndProcedure

	; Private procedures

	Procedure _Load(Path.s)
		CurrentPath = Path
		SetGadgetText(PathLabel, Path)
		SetGadgetText(StatusLabel, "Loading...")
		ClearGadgetItems(ListView)
		EntryCount = 0
		FS::List(Path, @_ListCallback())
	EndProcedure

	Procedure _ListCallback(Success.i, DataString.s)
		If Not Success Or Not IsWindow(Window)
			Notify::Toast("Failed to load directory.", Notify::#Error)
			ProcedureReturn
		EndIf

		If Not ParseJSON(0, DataString)
			Notify::Toast("Could not read directory.", Notify::#Error)
			ProcedureReturn
		EndIf

		Protected Root.i  = JSONValue(0)
		Protected Count.i = JSONArraySize(Root)
		Protected i.i, Item.i

		ClearGadgetItems(ListView)
		EntryCount = 0

		For i = 0 To Count - 1
			If EntryCount >= #Max_Entries : Break : EndIf
			Item = GetJSONElement(Root, i)

			EntryIDs(EntryCount)   = Str(GetJSONInteger(GetJSONMember(Item, "id")))
			EntryNames(EntryCount) = GetJSONString(GetJSONMember(Item, "name"))
			EntryIsDir(EntryCount) = GetJSONInteger(GetJSONMember(Item, "is_dir"))

			Protected Prefix.s = "[F] "
			If EntryIsDir(EntryCount) : Prefix = "[D] " : EndIf
			AddGadgetItem(ListView, -1, Prefix + EntryNames(EntryCount))
			EntryCount + 1
		Next

		FreeJSON(0)

		Protected Status.s = Str(EntryCount) + " item"
		If EntryCount <> 1 : Status + "s" : EndIf
		SetGadgetText(StatusLabel, Status)
	EndProcedure

	Procedure _MkdirCallback(Success.i, DataString.s)
		If Not IsWindow(Window) : ProcedureReturn : EndIf
		_Load(CurrentPath)   ; refresh regardless — server may have created it
	EndProcedure

	Procedure _CreateFileCallback(Success.i, DataString.s)
		If Not IsWindow(Window) : ProcedureReturn : EndIf
		If Success And ParseJSON(0, DataString)
			Protected Root.i = JSONValue(0)
			If GetJSONBoolean(GetJSONMember(Root, "success"))
				Protected ID.s   = Str(GetJSONInteger(GetJSONMember(Root, "id")))
				Protected Path.s = CurrentPath
				If Right(Path, 1) <> "/" : Path + "/" : EndIf
				Path + EntryNames(0)   ; name was stored in EntryNames(0) temporarily
				FreeJSON(0)
				_Load(CurrentPath)
				TextEditor::Open(ID, Path)
				ProcedureReturn
			EndIf
			FreeJSON(0)
		EndIf
		_Load(CurrentPath)
	EndProcedure

	Procedure _OpenInputWindow(Mode.i)
		If IsWindow(InputWindow) : ProcedureReturn : EndIf
		InputMode = Mode

		Protected Title.s = "New Folder"
		If Mode = 1 : Title = "New File" : EndIf

		Protected X.i = WindowX(Window) + (#Window_W  / 2) - 150
		Protected Y.i = WindowY(Window) + (#Window_H / 2) - 45

		InputWindow = OpenWindow(#PB_Any, X, Y, 300, 90, Title,
		                          #PB_Window_TitleBar | #PB_Window_SystemMenu)
		TextGadget  (#PB_Any,  10, 12, 280, 20, "Name:")
		InputField = StringGadget(#PB_Any, 10, 32, 280, 24, "")
		ButtonNew = ButtonGadget(#PB_Any,  10, 62,  130, 24, "OK")     ; EventGadget index 2
		CancelBtn = ButtonGadget(#PB_Any, 150, 62, 140, 24, "Cancel")

		SetActiveGadget(InputField)
		BindGadgetEvent(ButtonNew, @Handler_InputConfirm())
		BindGadgetEvent(CancelBtn,   @Handler_InputCancel())
		BindEvent(#PB_Event_CloseWindow, @Handler_InputCancel(), InputWindow)
	EndProcedure

	Procedure _CloseInputWindow()
		If IsWindow(InputWindow)
			CloseWindow(InputWindow)
		EndIf
	EndProcedure

	; Event handlers

	Procedure Handler_Resize()
		Protected W.i = WindowWidth(Window)
		Protected H.i = WindowHeight(Window)
		ResizeGadget(PathLabel,       36,  4, W - 200, #Toolbar_Height - 8)
		ResizeGadget(NewFolderButton, W - 162, 4, 80, #Toolbar_Height - 8)
		ResizeGadget(NewFileButton,   W - 78,  4, 74, #Toolbar_Height - 8)
		ResizeGadget(ListView, 0, #Toolbar_Height, W, H - #Toolbar_Height - #Statusbar_Height)
		ResizeGadget(StatusLabel, 4, H - #Statusbar_Height + 4, W - 8, #Statusbar_Height - 8)
	EndProcedure

	Procedure Handler_Up()
		If StackDepth > 0
			StackDepth - 1
			_Load(PathStack(StackDepth))
		EndIf
	EndProcedure

	Procedure Handler_NewFolder()
		_OpenInputWindow(0)
	EndProcedure

	Procedure Handler_NewFile()
		_OpenInputWindow(1)
	EndProcedure

	Procedure Handler_ListView()
		If EventType() <> #PB_EventType_LeftDoubleClick : ProcedureReturn : EndIf

		Protected Index.i = GetGadgetState(ListView)
		If Index < 0 Or Index >= EntryCount : ProcedureReturn : EndIf

		If EntryIsDir(Index)
			; Push current path, navigate in
			PathStack(StackDepth) = CurrentPath
			StackDepth + 1
			Protected NewPath.s = CurrentPath
			If Right(NewPath, 1) <> "/" : NewPath + "/" : EndIf
			NewPath + EntryNames(Index)
			_Load(NewPath)
		Else
			; Open in text editor
			Protected Path.s = CurrentPath
			If Right(Path, 1) <> "/" : Path + "/" : EndIf
			Path + EntryNames(Index)
			TextEditor::Open(EntryIDs(Index), Path)
		EndIf
	EndProcedure

	Procedure Handler_Close()
		If IsWindow(InputWindow)
			UnbindEvent(#PB_Event_CloseWindow, @Handler_InputCancel(), InputWindow)
			UnbindGadgetEvent(ButtonNew, @Handler_InputConfirm())
			UnbindGadgetEvent(CancelBtn,  @Handler_InputCancel())
			_CloseInputWindow()
		EndIf
		
		UnbindGadgetEvent(UpButton,        @Handler_Up())
		UnbindGadgetEvent(NewFolderButton, @Handler_NewFolder())
		UnbindGadgetEvent(NewFileButton,   @Handler_NewFile())
		UnbindGadgetEvent(ListView,        @Handler_ListView())
		UnbindEvent(#PB_Event_SizeWindow,   @Handler_Resize(), Window)
		UnbindEvent(#PB_Event_CloseWindow,  @Handler_Close(),  Window)
		Desktop::Unregister(Window)
		InputWindow = 0
		Window = 0
	EndProcedure

	Procedure Handler_InputConfirm()
		Protected Name.s = Trim(GetGadgetText(InputField))
		If Name = "" : ProcedureReturn : EndIf

		Protected Path.s = CurrentPath
		If Right(Path, 1) <> "/" : Path + "/" : EndIf
		Path + Name

		_CloseInputWindow()

		If InputMode = 0
			FS::Mkdir(Path, @_MkdirCallback())
		Else
			; Stash name temporarily for the callback to build the full path
			EntryNames(0) = Name
			FS::Write("", Path, "", @_CreateFileCallback())
		EndIf
	EndProcedure

	Procedure Handler_InputCancel()
		_CloseInputWindow()
	EndProcedure

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