DeclareModule AppManager
	Declare Open()
EndDeclareModule

Module AppManager
	EnableExplicit
	
	;- Layout
	#Win_W       = 520
	#Win_H       = 380
	#Toolbar_H   = 40
	#Statusbar_H = 26
	#List_Row_H  = 48
	
	;- Colors
	Global Color_Bg       = RGB( 22,  22,  34)
	Global Color_Row_Odd  = RGB( 28,  28,  42)
	Global Color_Row_Even = RGB( 22,  22,  34)
	Global Color_Sel      = RGB( 52,  52,  72)
	Global Color_Accent   = RGB(100, 120, 255)
	Global Color_Text     = RGB(200, 200, 215)
	Global Color_Dim      = RGB(110, 110, 135)
	Global Color_Sep      = RGB( 48,  48,  65)
	
	Global Font_UI    = LoadFont(#PB_Any, "sans-serif", 13)
	Global Font_Small = LoadFont(#PB_Any, "sans-serif", 11)
	
	;- App list
	Structure InstalledApp
		ID.s
		Name.s
		Manifest.s
		Permissions.s
	EndStructure
	
	Global NewList _Apps.InstalledApp()
	
	;- State
	Global Window = 0
	Global _Canvas
	Global _StatusLabel
	Global _InstallBtn
	Global _LaunchBtn
	Global _UninstallBtn
	Global _Hover = -1
	Global _Selected = -1
	Global _PendingUninstall.s = ""
	
	;- Install dialog
	Global _InputWin  = 0
	Global _URLField
	Global _OKBtn
	Global _CancelBtn
	
	;- Private declarations
	Declare _Refresh()
	Declare _DrawList()
	Declare _UpdateButtons()
	Declare _Status(Msg.s)
	Declare _OpenInstallDialog()
	Declare _CloseInstallDialog()
	Declare _ListCallback(Success, DataString.s)
	Declare _InstallCallback(Success, DataString.s)
	Declare _UninstallCallback(Success, DataString.s)
	Declare _DoUninstall(Result)
	Declare Handler_Close()
	Declare Handler_Canvas()
	Declare Handler_Install()
	Declare Handler_Launch()
	Declare Handler_Uninstall()
	Declare Handler_InputOK()
	Declare Handler_InputCancel()
	Declare Handler_Resize()
	
	;- Public
	Procedure Open()
		If IsWindow(Window)
			SetActiveWindow(Window) : ProcedureReturn
		EndIf
		
		Window = OpenWindow(#PB_Any, 160, 100, #Win_W, #Win_H, "App Manager",
		                    #PB_Window_TitleBar | #PB_Window_SizeGadget | #PB_Window_SystemMenu)
		SetWindowColor(Window, Color_Bg)
		
		_InstallBtn   = ButtonGadget(#PB_Any,   4, 6, 100, #Toolbar_H - 12, "+ Install")
		_LaunchBtn    = ButtonGadget(#PB_Any, 112, 6,  80, #Toolbar_H - 12, "Launch")
		_UninstallBtn = ButtonGadget(#PB_Any, 200, 6,  90, #Toolbar_H - 12, "Uninstall")
		
		_Canvas = CanvasGadget(#PB_Any, 0, #Toolbar_H,
		                       #Win_W, #Win_H - #Toolbar_H - #Statusbar_H)
		
		_StatusLabel = TextGadget(#PB_Any, 4, #Win_H - #Statusbar_H + 4,
		                          #Win_W - 8, #Statusbar_H - 8, "")
		SetGadgetColor(_StatusLabel, #PB_Gadget_FrontColor, Color_Dim)
		SetGadgetColor(_StatusLabel, #PB_Gadget_BackColor,  Color_Bg)
		
		BindGadgetEvent(_InstallBtn,   @Handler_Install())
		BindGadgetEvent(_LaunchBtn,    @Handler_Launch())
		BindGadgetEvent(_UninstallBtn, @Handler_Uninstall())
		BindGadgetEvent(_Canvas,       @Handler_Canvas())
		BindEvent(#PB_Event_SizeWindow,  @Handler_Resize(), Window)
		BindEvent(#PB_Event_CloseWindow, @Handler_Close(),  Window)
		
		Desktop::Register("App Manager", Window, "📦")
		_UpdateButtons()
		_Refresh()
	EndProcedure
	
	;- Private
	Procedure _Refresh()
		_Status("Loading...")
		ClearList(_Apps())
		_Selected = -1
		_DrawList()
		HTTPRequest(#PB_HTTP_Get, "/api/apps/list", "", @_ListCallback())
	EndProcedure
	
	Procedure _DrawList()
		If Not IsGadget(_Canvas) : ProcedureReturn : EndIf
		If Not StartDrawing(CanvasOutput(_Canvas)) : ProcedureReturn : EndIf
		
		Protected W = OutputWidth()
		Protected H = OutputHeight()
		
		Box(0, 0, W, H, Color_Bg)
		
		If IsFont(Font_UI) : DrawingFont(FontID(Font_UI)) : EndIf
		Protected TH = TextHeight("A")
		
		ForEach _Apps()
			Protected i   = ListIndex(_Apps())
			Protected Y   = i * #List_Row_H
			If Y + #List_Row_H > H : Break : EndIf
			
			Protected RowBg
			If i = _Selected
				RowBg = Color_Sel
			ElseIf i = _Hover
				RowBg = RGB(40, 40, 58)
			ElseIf i % 2 = 0
				RowBg = Color_Row_Even
			Else
				RowBg = Color_Row_Odd
			EndIf
			
			Box(0, Y, W, #List_Row_H, RowBg)
			If i = _Selected : Box(0, Y, 3, #List_Row_H, Color_Accent) : EndIf
			
			DrawingMode(#PB_2DDrawing_Transparent)
			DrawText(14, Y + (#List_Row_H / 2) - TH - 1, _Apps()\Name, Color_Text)
			
			If IsFont(Font_Small) : DrawingFont(FontID(Font_Small)) : EndIf
			DrawText(14, Y + (#List_Row_H / 2) + 2, _Apps()\ID, Color_Dim)
			If IsFont(Font_UI) : DrawingFont(FontID(Font_UI)) : EndIf
			
			Box(0, Y + #List_Row_H - 1, W, 1, Color_Sep)
		Next
		
		If ListSize(_Apps()) = 0
			DrawingMode(#PB_2DDrawing_Transparent)
			If IsFont(Font_UI) : DrawingFont(FontID(Font_UI)) : EndIf
			DrawText((W - TextWidth("No apps installed")) / 2,
			         (H - TH) / 2, "No apps installed", Color_Dim)
		EndIf
		
		StopDrawing()
	EndProcedure
	
	Procedure _UpdateButtons()
		DisableGadget(_LaunchBtn,    Bool(_Selected < 0))
		DisableGadget(_UninstallBtn, Bool(_Selected < 0))
	EndProcedure
	
	Procedure _Status(Msg.s)
		If IsGadget(_StatusLabel) : SetGadgetText(_StatusLabel, Msg) : EndIf
	EndProcedure
	
	Procedure _OpenInstallDialog()
		If IsWindow(_InputWin) : ProcedureReturn : EndIf
		
		Protected X = WindowX(Window) + (#Win_W / 2) - 170
		Protected Y = WindowY(Window) + (#Win_H / 2) - 50
		
		_InputWin  = OpenWindow(#PB_Any, X, Y, 340, 100, "Install App from URL",
		                        #PB_Window_TitleBar | #PB_Window_SystemMenu)
		TextGadget(#PB_Any, 10, 14, 320, 18, "Package URL (.zip):")
		_URLField  = StringGadget(#PB_Any,  10, 34, 320, 26, "")
		_OKBtn     = ButtonGadget(#PB_Any,  10, 68, 150, 24, "Install")
		_CancelBtn = ButtonGadget(#PB_Any, 170, 68, 160, 24, "Cancel")
		SetActiveGadget(_URLField)
		
		BindGadgetEvent(_OKBtn,     @Handler_InputOK())
		BindGadgetEvent(_CancelBtn, @Handler_InputCancel())
		BindEvent(#PB_Event_CloseWindow, @Handler_InputCancel(), _InputWin)
	EndProcedure
	
	Procedure _CloseInstallDialog()
		If Not IsWindow(_InputWin) : ProcedureReturn : EndIf
		UnbindGadgetEvent(_OKBtn,     @Handler_InputOK())
		UnbindGadgetEvent(_CancelBtn, @Handler_InputCancel())
		UnbindEvent(#PB_Event_CloseWindow, @Handler_InputCancel(), _InputWin)
		CloseWindow(_InputWin)
		_InputWin = 0
	EndProcedure
	
	;- HTTP callbacks
	Procedure _ListCallback(Success, DataString.s)
		If Not IsWindow(Window) : ProcedureReturn : EndIf
		
		ClearList(_Apps())
		_Selected = -1
		
		If Success And ParseJSON(0, DataString)
			Protected Root  = JSONValue(0)
			Protected Total = JSONArraySize(Root)
			Protected i
			
			For i = 0 To Total - 1
				Protected Item     = GetJSONElement(Root, i)
				Protected ManiNode = GetJSONMember(Item, "manifest")
				
				AddElement(_Apps())
				_Apps()\ID = GetJSONString(GetJSONMember(Item, "app_id"))
				
				Protected N.s = GetJSONString(GetJSONMember(ManiNode, "name"))
				If N <> ""
					_Apps()\Name = N
				Else
					_Apps()\Name = _Apps()\ID
				EndIf
				
				; Re-serialise manifest to a JSON string
				Protected Icon.s = GetJSONString(GetJSONMember(ManiNode, "icon"))
				_Apps()\Manifest = "{" +
				                   ~"\"id\":\""       + GetJSONString(GetJSONMember(ManiNode, "id"))      + ~"\"," +
				                   ~"\"name\":\""     + GetJSONString(GetJSONMember(ManiNode, "name"))    + ~"\"," +
				                   ~"\"version\":\""  + GetJSONString(GetJSONMember(ManiNode, "version")) + ~"\"," +
				                   ~"\"icon\":\""     + ReplaceString(Icon, ~"\"", ~"\\\"")               + ~"\"," +
				                   ~"\"entry\":\""    + GetJSONString(GetJSONMember(ManiNode, "entry"))   + ~"\"}"
				
				; Re-serialise permissions array to a JSON string
				Protected PermNode  = GetJSONMember(Item, "permissions")
				Protected PermCount = JSONArraySize(PermNode)
				Protected j, Perms.s = "["
				For j = 0 To PermCount - 1
					If j > 0 : Perms + "," : EndIf
					Perms + ~"\"" + GetJSONString(GetJSONElement(PermNode, j)) + ~"\""
				Next
				_Apps()\Permissions = Perms + "]"
				
				; Sync with the desktop start menu
				Desktop::InstallThirdPartyApp(_Apps()\ID, _Apps()\Manifest,
				                              _Apps()\Permissions, Icon)
			Next
			FreeJSON(0)
			
			Protected Count = ListSize(_Apps())
			If Count = 1
				_Status("1 app installed")
			Else
				_Status(Str(Count) + " apps installed")
			EndIf
		Else
			_Status("Failed to load app list.")
		EndIf
		
		_DrawList()
		_UpdateButtons()
	EndProcedure
	
	Procedure _InstallCallback(Success, DataString.s)
		If Not IsWindow(Window) : ProcedureReturn : EndIf
		DisableGadget(_InstallBtn, #False)
		
		If Success And ParseJSON(0, DataString)
			Protected Root = JSONValue(0)
			If GetJSONBoolean(GetJSONMember(Root, "success"))
				Protected AppID.s = GetJSONString(GetJSONMember(Root, "app_id"))
				FreeJSON(0)
				Notify::Toast("Installed: " + AppID, Notify::#Success)
				_Refresh()
				ProcedureReturn
			EndIf
			Protected Err.s = GetJSONString(GetJSONMember(Root, "error"))
			FreeJSON(0)
			Notify::Toast(Err, Notify::#Error)
		Else
			Notify::Toast("Install failed.", Notify::#Error)
		EndIf
		_Status("Ready.")
	EndProcedure
	
	Procedure _UninstallCallback(Success, DataString.s)
		If Not IsWindow(Window) : ProcedureReturn : EndIf
		If Success And ParseJSON(0, DataString)
			If GetJSONBoolean(GetJSONMember(JSONValue(0), "success"))
				FreeJSON(0)
				Notify::Toast("App uninstalled.", Notify::#Success)
				Desktop::UninstallThirdPartyApp(_PendingUninstall)
				_PendingUninstall = ""
				_Refresh()
				ProcedureReturn
			EndIf
			FreeJSON(0)
		EndIf
		Notify::Toast("Uninstall failed.", Notify::#Error)
		_PendingUninstall = ""
		_Status("Ready.")
	EndProcedure
	
	;- Event handlers
	Procedure Handler_Canvas()
		Protected EType = EventType()
		Protected MY    = GetGadgetAttribute(_Canvas, #PB_Canvas_MouseY)
		Protected Row   = MY / #List_Row_H
		
		Select EType
			Case #PB_EventType_MouseMove
				If Row >= ListSize(_Apps()) : Row = -1 : EndIf
				If Row <> _Hover
					_Hover = Row : _DrawList()
				EndIf
				
			Case #PB_EventType_MouseLeave
				If _Hover <> -1 : _Hover = -1 : _DrawList() : EndIf
				
			Case #PB_EventType_LeftButtonUp
				If Row >= 0 And Row < ListSize(_Apps())
					_Selected = Row : _DrawList() : _UpdateButtons()
				EndIf
				
			Case #PB_EventType_LeftDoubleClick
				If Row >= 0 And Row < ListSize(_Apps())
					_Selected = Row
					Handler_Launch()
				EndIf
		EndSelect
	EndProcedure
	
	Procedure Handler_Install()
		_OpenInstallDialog()
	EndProcedure
	
	Procedure Handler_Launch()
		If _Selected < 0 Or Not SelectElement(_Apps(), _Selected) : ProcedureReturn : EndIf
		AppRuntime::Launch(_Apps()\ID, _Apps()\Manifest, _Apps()\Permissions)
	EndProcedure
	
	Procedure Handler_Uninstall()
		If _Selected < 0 Or Not SelectElement(_Apps(), _Selected) : ProcedureReturn : EndIf
		Notify::Confirm("Uninstall " + _Apps()\Name,
		                "Remove this app? Private storage will also be deleted.",
		                @_DoUninstall())
	EndProcedure
	
	Procedure _DoUninstall(Result)
		If Not Result Or _Selected < 0 : ProcedureReturn : EndIf
		If Not SelectElement(_Apps(), _Selected) : ProcedureReturn : EndIf
		_PendingUninstall = _Apps()\ID
		_Status("Uninstalling " + _Apps()\ID + "...")
		HTTPRequest(#PB_HTTP_Post, "/api/apps/uninstall",
		            "app_id=" + URLEncoder(_Apps()\ID, #PB_UTF8) + "&keep_data=0",
		            @_UninstallCallback())
	EndProcedure
	
	Procedure Handler_InputOK()
		Protected URL.s = Trim(GetGadgetText(_URLField))
		If URL = "" : ProcedureReturn : EndIf
		_CloseInstallDialog()
		_Status("Installing...")
		DisableGadget(_InstallBtn, #True)
		HTTPRequest(#PB_HTTP_Post, "/api/apps/install",
		            "url=" + URLEncoder(URL, #PB_UTF8), @_InstallCallback())
	EndProcedure
	
	Procedure Handler_InputCancel()
		_CloseInstallDialog()
	EndProcedure
	
	Procedure Handler_Resize()
		Protected W = WindowWidth(Window)
		Protected H = WindowHeight(Window)
		ResizeGadget(_Canvas,      0, #Toolbar_H, W, H - #Toolbar_H - #Statusbar_H)
		ResizeGadget(_StatusLabel, 4, H - #Statusbar_H + 4, W - 8, #Statusbar_H - 8)
		_DrawList()
	EndProcedure
	
	Procedure Handler_Close()
		_CloseInstallDialog()
		UnbindGadgetEvent(_InstallBtn,   @Handler_Install())
		UnbindGadgetEvent(_LaunchBtn,    @Handler_Launch())
		UnbindGadgetEvent(_UninstallBtn, @Handler_Uninstall())
		UnbindGadgetEvent(_Canvas,       @Handler_Canvas())
		UnbindEvent(#PB_Event_SizeWindow,  @Handler_Resize(), Window)
		UnbindEvent(#PB_Event_CloseWindow, @Handler_Close(),  Window)
		Desktop::Unregister(Window)
		Window = 0
	EndProcedure
	
EndModule

; IDE Options = SpiderBasic 3.10 (Windows - x86)
; CursorPosition = 4
; FirstLine = 30
; Folding = DAA5
; iOSAppOrientation = 0
; AndroidAppCode = 0
; AndroidAppOrientation = 0
; EnableXP
; DPIAware
; CompileSourceDirectory