Option Strict On
Option Explicit On

Imports System
Imports System.Collections.Generic
Imports Nexamas.UI.Components
Imports Nexamas.UI.Controls
Imports Nexamas.UI.Layout

Namespace Nexamas.UI.Application

    ''' <summary>
    ''' Official same-window Application page host. It keeps one MASApplicationWindow shell alive and swaps only the
    ''' workspace content through LayoutApplicationSurface, using MAS TreeView navigation, MASLayout/Size tokens,
    ''' and Application-owned page metadata instead of top-level-window navigation or retained inactive page panels.
    ''' </summary>
    Public NotInheritable Class MASApplicationPageHost
        Implements IDisposable

        Private ReadOnly _window As MASApplicationWindow
        Private ReadOnly _pages As New List(Of MASApplicationPageRegistration)()
        Private ReadOnly _pageById As New Dictionary(Of String, MASApplicationPageRegistration)(StringComparer.OrdinalIgnoreCase)
        Private ReadOnly _navigationTree As MASTreeView
        Private ReadOnly _nodePageIds As New Dictionary(Of MASTreeNode, String)()
        Private ReadOnly _navigationRailBindings As New List(Of NavigationRailBinding)()
        Private ReadOnly _pageBreadcrumb As MASBreadcrumb
        Private ReadOnly _pageTitle As MASTitle
        Private ReadOnly _pageDescription As MASLabel
        Private _currentPageId As String = String.Empty
        Private _isShown As Boolean
        Private _navigationVisible As Boolean = True
        Private _breadcrumbVisible As Boolean
        Private _breadcrumbRootText As String = "Pages"
        Private _regionBackgroundsVisible As Boolean = True
        Private _rhythm As MASApplicationSurfaceRhythm = MASApplicationSurfaceRhythm.Comfortable
        Private _navigationSize As MASApplicationSurfaceSideSize = MASApplicationSurfaceSideSize.[Default]
        Private _updatingNavigationSelection As Boolean
        Private _updatingNavigationRailSelection As Boolean
        Private _updatingBreadcrumbSelection As Boolean
        Private _disposed As Boolean

        Public Event CurrentPageChanged As EventHandler

        Private NotInheritable Class NavigationRailBinding
            Friend Sub New(rail As MASNavigationRail,
                           handler As EventHandler)
                Me.Rail = rail
                Me.Handler = handler
            End Sub

            Friend ReadOnly Property Rail As MASNavigationRail
            Friend ReadOnly Property Handler As EventHandler
        End Class

        Friend Sub New(window As MASApplicationWindow)
            If window Is Nothing Then Throw New ArgumentNullException(NameOf(window))

            _window = window
            _navigationTree = MASTreeView.Create().WithoutNodeIcons().WithSize(MASSize.FillWidth)
            _pageBreadcrumb = MASBreadcrumb.Create().WithSize(MASSize.FillWidth)
            _pageTitle = MASTitle.Create(String.Empty).WithDivider(True).WithSize(MASSize.FillWidth)
            _pageDescription = MASLabel.Create(String.Empty).WithSize(MASSize.FillWidth)
            _pageDescription.LabelVariant = MASLabelVariant.Body
            _pageDescription.TextRole = MASLabelTextRole.Secondary
            _pageDescription.MultiLine = True
            _pageDescription.VerticalAlignment = MASVerticalAlignment.Top
            _pageDescription.MaxLines = 3

            AddHandler _navigationTree.SelectedNodeChanged, AddressOf NavigationTree_SelectedNodeChanged
            AddHandler _pageBreadcrumb.SelectedIndexChanged, AddressOf PageBreadcrumb_SelectedIndexChanged
        End Sub

        Public ReadOnly Property PageCount As Integer
            Get
                ThrowIfDisposed()
                Return _pages.Count
            End Get
        End Property

        Public ReadOnly Property CurrentPageId As String
            Get
                ThrowIfDisposed()
                Return _currentPageId
            End Get
        End Property

        Public ReadOnly Property CurrentPageTitle As String
            Get
                ThrowIfDisposed()
                Dim page As MASApplicationPageRegistration = ResolveCurrentPage()
                Return If(page Is Nothing, String.Empty, page.Title)
            End Get
        End Property

        Public ReadOnly Property CurrentPageDescription As String
            Get
                ThrowIfDisposed()
                Dim page As MASApplicationPageRegistration = ResolveCurrentPage()
                Return If(page Is Nothing, String.Empty, page.Description)
            End Get
        End Property

        Public ReadOnly Property IsShown As Boolean
            Get
                ThrowIfDisposed()
                Return _isShown
            End Get
        End Property

        ''' <summary>
        ''' Registers a same-window page. The build action is cached by the page host after its first use, so pages
        ''' should describe their MAS controls through MASApplicationLayoutPageBuilder instead of top-level-window
        ''' navigation, manual bounds, or retained inactive panels.
        ''' </summary>
        Public Function RegisterPage(pageId As String,
                                     title As String,
                                     buildPage As Action(Of MASApplicationLayoutPageBuilder),
                                     Optional groupName As String = Nothing,
                                     Optional description As String = Nothing) As MASApplicationPageHost
            ThrowIfDisposed()

            Dim registration As New MASApplicationPageRegistration(pageId, title, groupName, description, buildPage)
            If Not registration.IsValid Then Throw New ArgumentException("A MASApplicationWindow page requires page id, title, and build action.", NameOf(pageId))

            If _pageById.ContainsKey(registration.PageId) Then
                Throw New InvalidOperationException("A MASApplicationWindow page is already registered: " & registration.PageId)
            End If

            _pageById.Add(registration.PageId, registration)
            _pages.Add(registration)

            If _currentPageId.Length = 0 Then _currentPageId = registration.PageId
            RebuildNavigationTree()
            RebuildNavigationRailBindings()
            If _isShown Then ApplyPageHostSurface()
            Return Me
        End Function

        Public Function ClearPages() As MASApplicationPageHost
            ThrowIfDisposed()

            For Each page As MASApplicationPageRegistration In _pages
                page.InvalidateCachedPage()
            Next

            _pages.Clear()
            _pageById.Clear()
            _nodePageIds.Clear()
            _navigationTree.ClearNodes()
            _currentPageId = String.Empty
            RebuildNavigationRailBindings()
            If _isShown Then ApplyPageHostSurface()
            Return Me
        End Function

        Public Function ContainsPage(pageId As String) As Boolean
            ThrowIfDisposed()
            Return _pageById.ContainsKey(MASApplicationPageRegistration.NormalizePageId(pageId))
        End Function

        Public Function NavigateTo(pageId As String) As Boolean
            ThrowIfDisposed()
            Dim normalizedId As String = MASApplicationPageRegistration.NormalizePageId(pageId)
            If normalizedId.Length = 0 OrElse Not _pageById.ContainsKey(normalizedId) Then Return False

            Dim previousPage As MASApplicationPageRegistration = ResolveCurrentPage()
            Dim changed As Boolean = Not String.Equals(_currentPageId, normalizedId, StringComparison.OrdinalIgnoreCase)

            If changed AndAlso previousPage IsNot Nothing Then
                previousPage.InvalidateCachedPage()
            End If

            _currentPageId = normalizedId
            SyncNavigationSelection()
            SyncNavigationRailBindings()
            Dim applied As Boolean = If(_isShown, ApplyPageHostSurface(), True)

            If changed Then RaiseEvent CurrentPageChanged(Me, EventArgs.Empty)
            Return applied
        End Function

        Public Function Show() As Boolean
            ThrowIfDisposed()
            If _currentPageId.Length = 0 AndAlso _pages.Count > 0 Then _currentPageId = _pages(0).PageId
            _isShown = True
            RebuildNavigationTree()
            RebuildNavigationRailBindings()
            Return ApplyPageHostSurface()
        End Function

        Public Function Refresh() As Boolean
            ThrowIfDisposed()
            If Not _isShown Then Return False
            Return ApplyPageHostSurface()
        End Function

        Public Function WithNavigationVisible(visible As Boolean) As MASApplicationPageHost
            ThrowIfDisposed()
            _navigationVisible = visible
            If _isShown Then ApplyPageHostSurface()
            Return Me
        End Function

        ''' <summary>
        ''' Shows or hides the official MASBreadcrumb path trail in the PageHost workspace.
        ''' The trail is rendered by MASBreadcrumb and navigates only to already registered
        ''' MASApplicationWindow.Pages routes, so consuming applications do not need a
        ''' secondary breadcrumb renderer, router, or manual page-position label.
        ''' </summary>
        Public Function WithBreadcrumbVisible(visible As Boolean) As MASApplicationPageHost
            ThrowIfDisposed()
            _breadcrumbVisible = visible
            If _isShown Then ApplyPageHostSurface()
            Return Me
        End Function

        ''' <summary>
        ''' Sets the first segment shown by the PageHost MASBreadcrumb path trail.
        ''' Blank values fall back to the neutral "Pages" segment.
        ''' </summary>
        Public Function WithBreadcrumbRootText(rootText As String) As MASApplicationPageHost
            ThrowIfDisposed()
            Dim normalized As String = NormalizeBreadcrumbText(rootText)
            If normalized.Length = 0 Then normalized = "Pages"
            _breadcrumbRootText = normalized
            If _isShown Then ApplyPageHostSurface()
            Return Me
        End Function

        Public Function WithRegionBackgrounds(visible As Boolean) As MASApplicationPageHost
            ThrowIfDisposed()
            _regionBackgroundsVisible = visible
            If _isShown Then ApplyPageHostSurface()
            Return Me
        End Function

        Public Function WithRhythm(rhythm As MASApplicationSurfaceRhythm) As MASApplicationPageHost
            ThrowIfDisposed()
            _rhythm = rhythm
            If _isShown Then ApplyPageHostSurface()
            Return Me
        End Function

        Public Function WithNavigationSize(size As MASApplicationSurfaceSideSize) As MASApplicationPageHost
            ThrowIfDisposed()
            _navigationSize = size
            If _isShown Then ApplyPageHostSurface()
            Return Me
        End Function

        Public Function BindNavigationRail(rail As MASNavigationRail) As MASApplicationPageHost
            ThrowIfDisposed()
            If rail Is Nothing Then Return Me
            ReleaseNavigationRailBinding(rail)
            Dim handler As EventHandler = Sub(sender As Object, e As EventArgs)
                                              NavigationRail_SelectedItemChanged(rail)
                                          End Sub
            AddHandler rail.SelectedItemChanged, handler
            _navigationRailBindings.Add(New NavigationRailBinding(rail, handler))
            RebuildNavigationRailBinding(rail)
            Return Me
        End Function

        Private Function ApplyPageHostSurface() As Boolean
            Dim currentPage As MASApplicationPageRegistration = ResolveCurrentPage()
            SyncPageBreadcrumb(currentPage)
            _pageTitle.Text = If(currentPage Is Nothing, "MASApplication Pages", currentPage.Title)
            _pageDescription.Text = If(currentPage Is Nothing, "Register pages, then navigate inside this window through the Application workspace host.", currentPage.Description)

            Return _window.Controls.LayoutApplicationSurface(
                Sub(surface As MASApplicationSurfaceLayoutBuilder)
                    surface.ApplicationChrome(_window.Shell).
                        Rhythm(_rhythm).
                        ShowRegionBackgrounds(_regionBackgroundsVisible).
                        SidebarSize(_navigationSize).
                        CommandBarMainAreaTop().
                        FooterMainAreaBottom()

                    If _navigationVisible Then
                        surface.Navigation(
                            Sub(navigationPage As MASApplicationLayoutPageBuilder)
                                navigationPage.Spacing(MASLayoutSpacing.Small).
                                    Padding(MASLayoutSpacing.Small).
                                    FullWidth(_navigationTree, MASSize.FillWidth)
                            End Sub)
                    End If

                    surface.Workspace(
                        Sub(workspacePage As MASApplicationLayoutPageBuilder)
                            workspacePage.EnableVerticalScrollInternal("MASApplicationWindow.Pages.Workspace." & _currentPageId).
                                Spacing(MASLayoutSpacing.Large).
                                Padding(MASLayoutSpacing.Large)

                            If _breadcrumbVisible Then
                                workspacePage.FullWidth(_pageBreadcrumb, MASSize.FillWidth)
                            End If

                            workspacePage.FullWidth(_pageTitle, MASSize.FillWidth)

                            If _pageDescription.Text.Length > 0 Then
                                workspacePage.FullWidth(_pageDescription, MASSize.FillWidth)
                            End If

                            If currentPage IsNot Nothing Then
                                workspacePage.IncludePage(currentPage.GetOrBuildPage())
                            End If
                        End Sub)
                End Sub)
        End Function

        Private Sub SyncPageBreadcrumb(currentPage As MASApplicationPageRegistration)
            If _pageBreadcrumb Is Nothing Then Return

            Dim items As New List(Of String)()
            items.Add(If(_breadcrumbRootText.Length = 0, "Pages", _breadcrumbRootText))

            If currentPage IsNot Nothing Then
                If currentPage.GroupName.Length > 0 Then items.Add(currentPage.GroupName)
                If currentPage.Title.Length > 0 Then items.Add(currentPage.Title)
            End If

            _updatingBreadcrumbSelection = True
            Try
                _pageBreadcrumb.SetItems(items)
                _pageBreadcrumb.WithSelectedIndex(Math.Max(0, items.Count - 1))
            Finally
                _updatingBreadcrumbSelection = False
            End Try
        End Sub

        Private Sub PageBreadcrumb_SelectedIndexChanged(sender As Object, e As EventArgs)
            If _disposed Then Return
            If _updatingBreadcrumbSelection OrElse Not _breadcrumbVisible Then Return
            If _pageBreadcrumb Is Nothing Then Return

            Dim selectedIndex As Integer = _pageBreadcrumb.SelectedIndex
            Dim currentPage As MASApplicationPageRegistration = ResolveCurrentPage()

            If selectedIndex <= 0 Then
                If _pages.Count > 0 Then NavigateTo(_pages(0).PageId)
                Return
            End If

            If currentPage IsNot Nothing AndAlso
               currentPage.GroupName.Length > 0 AndAlso
               selectedIndex = 1 Then
                Dim firstGroupPage As MASApplicationPageRegistration = ResolveFirstPageInGroup(currentPage.GroupName)
                If firstGroupPage IsNot Nothing Then NavigateTo(firstGroupPage.PageId)
            End If
        End Sub

        Private Function ResolveFirstPageInGroup(groupName As String) As MASApplicationPageRegistration
            Dim normalizedGroupName As String = NormalizeBreadcrumbText(groupName)
            If normalizedGroupName.Length = 0 Then Return Nothing

            For Each page As MASApplicationPageRegistration In _pages
                If page IsNot Nothing AndAlso
                   String.Equals(page.GroupName, normalizedGroupName, StringComparison.OrdinalIgnoreCase) Then
                    Return page
                End If
            Next

            Return Nothing
        End Function

        Private Shared Function NormalizeBreadcrumbText(value As String) As String
            Return If(value, String.Empty).Trim()
        End Function

        Private Function ResolveCurrentPage() As MASApplicationPageRegistration
            If _currentPageId.Length = 0 Then Return Nothing
            Dim page As MASApplicationPageRegistration = Nothing
            If _pageById.TryGetValue(_currentPageId, page) Then Return page
            Return Nothing
        End Function

        Private Sub RebuildNavigationTree()
            _nodePageIds.Clear()
            _navigationTree.ClearNodes()

            Dim groups As New Dictionary(Of String, MASTreeNode)(StringComparer.OrdinalIgnoreCase)
            For Each page As MASApplicationPageRegistration In _pages
                Dim parent As MASTreeNode = Nothing
                If page.GroupName.Length > 0 Then
                    If Not groups.TryGetValue(page.GroupName, parent) Then
                        parent = _navigationTree.AddRoot(page.GroupName)
                        parent.IsExpanded = True
                        groups.Add(page.GroupName, parent)
                    End If

                    Dim child As MASTreeNode = parent.AddChild(page.Title)
                    child.Tag = page.PageId
                    _nodePageIds(child) = page.PageId
                Else
                    Dim root As MASTreeNode = _navigationTree.AddRoot(page.Title)
                    root.Tag = page.PageId
                    _nodePageIds(root) = page.PageId
                End If
            Next

            SyncNavigationSelection()
        End Sub

        Private Sub SyncNavigationSelection()
            If _updatingNavigationSelection Then Return

            _updatingNavigationSelection = True
            Try
                For Each pair As KeyValuePair(Of MASTreeNode, String) In _nodePageIds
                    If String.Equals(pair.Value, _currentPageId, StringComparison.OrdinalIgnoreCase) Then
                        If Not Object.ReferenceEquals(_navigationTree.SelectedNode, pair.Key) Then
                            _navigationTree.SelectedNode = pair.Key
                        End If
                        Exit For
                    End If
                Next
            Finally
                _updatingNavigationSelection = False
            End Try
        End Sub

        Private Sub RebuildNavigationRailBindings()
            For Each binding As NavigationRailBinding In _navigationRailBindings
                RebuildNavigationRailBinding(binding.Rail)
            Next
        End Sub

        Private Sub RebuildNavigationRailBinding(rail As MASNavigationRail)
            If rail Is Nothing Then Return
            _updatingNavigationRailSelection = True
            Try
                rail.ClearItems()
                For Each page As MASApplicationPageRegistration In _pages
                    Dim label As String = If(page.GroupName.Length > 0, page.GroupName & " / " & page.Title, page.Title)
                    rail.AddItem(page.PageId, label)
                Next
                If _currentPageId.Length > 0 Then rail.SelectItem(_currentPageId)
            Finally
                _updatingNavigationRailSelection = False
            End Try
        End Sub

        Private Sub SyncNavigationRailBindings()
            If _updatingNavigationRailSelection Then Return
            _updatingNavigationRailSelection = True
            Try
                For Each binding As NavigationRailBinding In _navigationRailBindings
                    If binding.Rail IsNot Nothing AndAlso _currentPageId.Length > 0 Then
                        binding.Rail.SelectItem(_currentPageId)
                    End If
                Next
            Finally
                _updatingNavigationRailSelection = False
            End Try
        End Sub

        Private Sub ReleaseNavigationRailBinding(rail As MASNavigationRail)
            If rail Is Nothing Then Return
            For i As Integer = _navigationRailBindings.Count - 1 To 0 Step -1
                Dim binding As NavigationRailBinding = _navigationRailBindings(i)
                If Object.ReferenceEquals(binding.Rail, rail) Then
                    RemoveHandler rail.SelectedItemChanged, binding.Handler
                    _navigationRailBindings.RemoveAt(i)
                End If
            Next
        End Sub

        Private Sub NavigationRail_SelectedItemChanged(rail As MASNavigationRail)
            If _disposed Then Return
            If _updatingNavigationRailSelection Then Return
            If rail Is Nothing Then Return
            Dim pageId As String = MASApplicationPageRegistration.NormalizePageId(rail.SelectedItemKey)
            If pageId.Length = 0 Then Return
            NavigateTo(pageId)
        End Sub

        Private Sub NavigationTree_SelectedNodeChanged(sender As Object, e As EventArgs)
            If _disposed Then Return
            If _updatingNavigationSelection Then Return

            Dim selected As MASTreeNode = _navigationTree.SelectedNode
            If selected Is Nothing Then Return

            Dim pageId As String = Nothing
            If _nodePageIds.TryGetValue(selected, pageId) Then
                NavigateTo(pageId)
            End If
        End Sub

        Private Sub ThrowIfDisposed()
            If _disposed Then Throw New ObjectDisposedException(NameOf(MASApplicationPageHost))
            If _window IsNot Nothing AndAlso _window.IsDisposed Then Throw New ObjectDisposedException(NameOf(MASApplicationWindow))
        End Sub

        Public Sub Dispose() Implements IDisposable.Dispose
            If _disposed Then Return
            _disposed = True

            Try
                RemoveHandler _navigationTree.SelectedNodeChanged, AddressOf NavigationTree_SelectedNodeChanged
            Catch masCaughtExceptionPageHostNavigationTreeDispose As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDisposeCleanup(masCaughtExceptionPageHostNavigationTreeDispose)
            End Try

            For i As Integer = _navigationRailBindings.Count - 1 To 0 Step -1
                Dim binding As NavigationRailBinding = _navigationRailBindings(i)
                If binding Is Nothing OrElse binding.Rail Is Nothing Then Continue For

                Try
                    RemoveHandler binding.Rail.SelectedItemChanged, binding.Handler
                Catch masCaughtExceptionPageHostRailDispose As Exception
                    Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDisposeCleanup(masCaughtExceptionPageHostRailDispose)
                End Try
            Next

            _navigationRailBindings.Clear()

            For Each page As MASApplicationPageRegistration In _pages
                page.InvalidateCachedPage()
            Next

            Try
                _window.ControlsCoreForDispose.ReleaseRetainedLayoutForDirectMutationInternal()
            Catch masCaughtExceptionPageHostRetainedLayoutDispose As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDisposeCleanup(
                    masCaughtExceptionPageHostRetainedLayoutDispose,
                    "MASApplicationPageHost.Dispose.RetainedLayout")
            End Try

            _pages.Clear()
            _pageById.Clear()
            _nodePageIds.Clear()
            _currentPageId = String.Empty
            _isShown = False

            DisposeOwnedPageHostControl(_navigationTree, "NavigationTree")
            DisposeOwnedPageHostControl(_pageBreadcrumb, "PageBreadcrumb")
            DisposeOwnedPageHostControl(_pageTitle, "PageTitle")
            DisposeOwnedPageHostControl(_pageDescription, "PageDescription")
        End Sub

        Private Shared Sub DisposeOwnedPageHostControl(control As MASControlBase,
                                                       ownerName As String)
            If control Is Nothing Then Return

            Try
                control.Dispose()
            Catch masCaughtExceptionPageHostOwnedControlDispose As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDisposeCleanup(
                    masCaughtExceptionPageHostOwnedControlDispose,
                    "MASApplicationPageHost.Dispose." & If(ownerName, "Control"))
            End Try
        End Sub
    End Class

End Namespace
