Option Strict On
Option Explicit On

Imports System
Imports System.Collections.Generic
Imports System.IO
Imports System.Linq
Imports Nexamas.UI.FileSystem
Imports Nexamas.UI.General

Namespace Nexamas.UI.Components

    ''' <summary>
    ''' Coordinates FilePicker behavior independently from any host.
    ''' </summary>
    ''' <remarks>
    ''' FilePickerController owns FilePicker behavior only.
    '''
    ''' It does not know about Overlay.
    ''' It does not know about Float.
    ''' It does not close any host.
    ''' It does not manage modal/shield ownership.
    '''
    ''' The controller produces MASFilePickerResult and raises Accepted/Cancelled.
    ''' The external host decides what to do with those events.
    ''' </remarks>
    Friend NotInheritable Class FilePickerController
        Implements IDisposable

        Private ReadOnly _browser As MASFileBrowser
        Private ReadOnly _options As MASFilePickerOptions

        Private ReadOnly _pickerPolicy As FilePickerSelectionPolicy
        Private ReadOnly _browserSelectionPolicy As MASFileBrowserSelectionPolicy

        Private ReadOnly _history As New MASNavigationHistory(Of String)(
            Function(path As String) path Is Nothing
        )

        Private _saveFileName As String = String.Empty
        Private _disposed As Boolean
        Private _eventsWired As Boolean

        Friend Event Accepted As EventHandler(Of MASFilePickerResultEventArgs)
        Friend Event Cancelled As EventHandler
        Friend Event StateChanged As EventHandler

        Friend Sub New(options As MASFilePickerOptions)
            Me.New(options, Nothing)
        End Sub

        Friend Sub New(options As MASFilePickerOptions, provider As IFileSystemProvider)
            _options = MASFilePickerOptions.SafeClone(options)
            _options.Normalize()

            _pickerPolicy = FilePickerSelectionPolicy.CreateForOptions(_options)
            _browserSelectionPolicy = CreateBrowserSelectionPolicy(_options)

            _browser = New MASFileBrowser()
            _browser.SelectionPolicy = _browserSelectionPolicy
            _browser.ShowHidden = _options.ShowHidden
            _browser.Filter = _options.Filter
            If provider IsNot Nothing Then _browser.Provider = provider

            If _options.Mode = MASFilePickerMode.SaveFile Then
                _saveFileName = If(_options.FileName, String.Empty)
            End If

            ApplyInitialPath()
            WireEvents()
            RaiseStateChanged()
        End Sub

        Friend ReadOnly Property Browser As MASFileBrowser
            Get
                Return _browser
            End Get
        End Property

        Friend ReadOnly Property Options As MASFilePickerOptions
            Get
                Return _options
            End Get
        End Property

        ''' <summary>
        ''' FilePicker behavior policy.
        ''' </summary>
        ''' <remarks>
        ''' This is not passed to MASFileBrowser.
        ''' MASFileBrowser receives MASFileBrowserSelectionPolicy only.
        ''' </remarks>
        Friend ReadOnly Property SelectionPolicy As FilePickerSelectionPolicy
            Get
                Return _pickerPolicy
            End Get
        End Property

        Friend ReadOnly Property BrowserSelectionPolicy As MASFileBrowserSelectionPolicy
            Get
                Return _browserSelectionPolicy
            End Get
        End Property

        Friend ReadOnly Property CanGoBack As Boolean
            Get
                Return _history.CanGoBack
            End Get
        End Property

        Friend ReadOnly Property CanGoForward As Boolean
            Get
                Return _history.CanGoForward
            End Get
        End Property

        Friend Property SaveFileName As String
            Get
                Return _saveFileName
            End Get
            Set(value As String)
                Dim v As String = If(value, String.Empty)

                If String.Equals(_saveFileName, v, StringComparison.Ordinal) Then Return

                _saveFileName = v
                RaiseStateChanged()
            End Set
        End Property

        Friend ReadOnly Property DialogTitle As String
            Get
                If Not String.IsNullOrWhiteSpace(_options.Title) Then Return _options.Title

                Select Case _options.Mode
                    Case MASFilePickerMode.OpenFile
                        Return If(_options.AllowMultiSelect, "Select Files", "Select File")

                    Case MASFilePickerMode.SaveFile
                        Return "Save File"

                    Case MASFilePickerMode.PickTargets
                        Return "Select Targets"

                    Case Else
                        Return "File Picker"
                End Select
            End Get
        End Property

        Friend ReadOnly Property PrimaryButtonText As String
            Get
                If Not String.IsNullOrWhiteSpace(_options.PrimaryButtonTextOverride) Then
                    Return _options.PrimaryButtonTextOverride
                End If

                Select Case _options.Mode
                    Case MASFilePickerMode.OpenFile
                        Return "Select"

                    Case MASFilePickerMode.SaveFile
                        Return "Save"

                    Case MASFilePickerMode.PickTargets
                        Return "Select"

                    Case Else
                        Return "OK"
                End Select
            End Get
        End Property

        Friend ReadOnly Property IsSaveMode As Boolean
            Get
                Return _options.Mode = MASFilePickerMode.SaveFile
            End Get
        End Property

        Friend ReadOnly Property CurrentPath As String
            Get
                If _browser Is Nothing Then Return String.Empty
                Return _browser.CurrentPath
            End Get
        End Property

        Friend ReadOnly Property SelectedItem As FileSystemEntry
            Get
                If _browser Is Nothing Then Return Nothing
                Return _browser.SelectedItem
            End Get
        End Property

        Friend ReadOnly Property SelectedItems As List(Of FileSystemEntry)
            Get
                If _browser Is Nothing Then Return New List(Of FileSystemEntry)()
                Return _browser.SelectedItems.ToList()
            End Get
        End Property

        Friend ReadOnly Property CanAccept As Boolean
            Get
                Select Case _options.Mode
                    Case MASFilePickerMode.OpenFile
                        Return GetAcceptedSelectedEntries().Count > 0

                    Case MASFilePickerMode.PickTargets
                        Return GetAcceptedSelectedEntries().Count > 0 OrElse CanAcceptCurrentPath()

                    Case MASFilePickerMode.SaveFile
                        Return CanAcceptSaveFile()

                    Case Else
                        Return False
                End Select
            End Get
        End Property

        Friend Function AcceptSelection() As MASFilePickerResult
            ThrowIfDisposed()

            Dim result As MASFilePickerResult = BuildResult()

            If result IsNot Nothing AndAlso result.Accepted Then
                RaiseEvent Accepted(Me, New MASFilePickerResultEventArgs(result))
            End If

            Return result
        End Function

        Friend Sub CancelSelection()
            ThrowIfDisposed()
            RaiseEvent Cancelled(Me, EventArgs.Empty)
        End Sub

        Friend Sub NavigateTo(path As String)
            ThrowIfDisposed()
            NavigateToCore(If(path, String.Empty), recordCurrent:=True)
        End Sub

        Friend Sub LoadRoots()
            ThrowIfDisposed()
            NavigateToCore(String.Empty, recordCurrent:=True)
        End Sub

        Friend Function GoBack() As Boolean
            ThrowIfDisposed()

            Dim target As String = Nothing

            If Not _history.TryMoveBack(_browser.CurrentPath, target) Then
                Return False
            End If

            NavigateToCore(If(target, String.Empty), recordCurrent:=False)
            Return True
        End Function

        Friend Function GoForward() As Boolean
            ThrowIfDisposed()

            Dim target As String = Nothing

            If Not _history.TryMoveForward(_browser.CurrentPath, target) Then
                Return False
            End If

            NavigateToCore(If(target, String.Empty), recordCurrent:=False)
            Return True
        End Function

        Friend Sub NavigateUp()
            ThrowIfDisposed()

            If _browser Is Nothing Then Return
            If _browser.Provider Is Nothing Then Return
            If String.IsNullOrWhiteSpace(_browser.CurrentPath) Then Return

            Dim parentPath As String = _browser.Provider.GetParentPath(_browser.CurrentPath)

            If String.IsNullOrWhiteSpace(parentPath) Then
                LoadRoots()
            Else
                NavigateTo(parentPath)
            End If
        End Sub

        Friend Sub RefreshItems()
            ThrowIfDisposed()
            If _browser Is Nothing Then Return

            _browser.RefreshItems()
            RaiseStateChanged()
        End Sub

        Friend Sub SelectFirst()
            ThrowIfDisposed()

            If _browser Is Nothing Then Return

            If _browser.Items.Count <= 0 Then
                _browser.ClearSelection()
                Return
            End If

            For i As Integer = 0 To _browser.Items.Count - 1
                Dim it As FileSystemEntry = _browser.Items(i)

                If _browserSelectionPolicy IsNot Nothing AndAlso _browserSelectionPolicy.CanSelect(it) Then
                    _browser.SelectOnly(i)
                    _browser.EnsureVisible(i)
                    Exit Sub
                End If
            Next

            _browser.ClearSelection()
        End Sub

        Friend Sub UseSelectedFileNameForSave()
            ThrowIfDisposed()

            If _options.Mode <> MASFilePickerMode.SaveFile Then Return

            Dim item As FileSystemEntry = _browser.SelectedItem
            If item Is Nothing OrElse Not item.IsFile Then Return

            SaveFileName = item.Name
        End Sub

        Private Sub ApplyInitialPath()
            If _browser Is Nothing OrElse _browser.Provider Is Nothing Then
                If _browser IsNot Nothing Then _browser.LoadRoots()
                Return
            End If

            Dim path As String = If(_options.InitialPath, String.Empty).Trim()

            If String.IsNullOrWhiteSpace(path) Then
                _browser.LoadRoots()
                Return
            End If

            Dim provider As IFileSystemProvider = _browser.Provider

            Try
                If provider.DirectoryExists(path) Then
                    _browser.CurrentPath = path
                    Return
                End If

                If provider.FileExists(path) Then
                    Dim parent As String = provider.GetParentPath(path)

                    If Not String.IsNullOrWhiteSpace(parent) AndAlso provider.DirectoryExists(parent) Then
                        _browser.CurrentPath = parent

                        If _options.Mode = MASFilePickerMode.SaveFile Then
                            _saveFileName = IO.Path.GetFileName(path)
                        End If

                        Return
                    End If
                End If
            Catch masCaughtException1 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowFileSystemBoundary(masCaughtException1)
            End Try

            _browser.LoadRoots()
        End Sub

        Private Sub NavigateToCore(path As String, recordCurrent As Boolean)
            If _browser Is Nothing Then Return

            Dim target As String = If(path, String.Empty)
            Dim current As String = If(_browser.CurrentPath, String.Empty)

            If String.Equals(current, target, StringComparison.OrdinalIgnoreCase) Then Return

            If recordCurrent Then
                _history.RecordNavigation(current)
            End If

            If String.IsNullOrWhiteSpace(target) Then
                _browser.LoadRoots()
            Else
                _browser.CurrentPath = target
            End If

            RaiseStateChanged()
        End Sub

        Private Function BuildResult() As MASFilePickerResult
            Select Case _options.Mode
                Case MASFilePickerMode.OpenFile
                    Return BuildOpenFileResult()

                Case MASFilePickerMode.PickTargets
                    Return BuildTargetsResult()

                Case MASFilePickerMode.SaveFile
                    Return BuildSaveFileResult()

                Case Else
                    Return MASFilePickerResult.Cancelled()
            End Select
        End Function

        Private Function BuildOpenFileResult() As MASFilePickerResult
            Return MASFilePickerResult.FromPaths(
                GetAcceptedSelectedEntries().Select(Function(x) x.FullPath)
            )
        End Function

        Private Function BuildTargetsResult() As MASFilePickerResult
            Dim selected As List(Of FileSystemEntry) = GetAcceptedSelectedEntries()

            If selected.Count > 0 Then
                Return MASFilePickerResult.FromPaths(selected.Select(Function(x) x.FullPath))
            End If

            If CanAcceptCurrentPath() Then
                Return MASFilePickerResult.FromPath(_browser.CurrentPath)
            End If

            Return MASFilePickerResult.Cancelled()
        End Function

        Private Function BuildSaveFileResult() As MASFilePickerResult
            If Not CanAcceptSaveFile() Then Return MASFilePickerResult.Cancelled()

            Dim safeFileName As String = If(_saveFileName, String.Empty).Trim()
            Dim finalPath As String = _browser.Provider.CombinePath(_browser.CurrentPath, safeFileName)

            Return MASFilePickerResult.FromPath(finalPath)
        End Function

        Private Function GetAcceptedSelectedEntries() As List(Of FileSystemEntry)
            Dim result As New List(Of FileSystemEntry)()

            If _browser Is Nothing Then Return result

            For Each it As FileSystemEntry In _browser.SelectedItems
                If it Is Nothing Then Continue For
                If _pickerPolicy IsNot Nothing AndAlso _pickerPolicy.CanReturn(it) Then
                    result.Add(it)
                End If
            Next

            Return result
        End Function

        Private Function CanAcceptSaveFile() As Boolean
            If _options.Mode <> MASFilePickerMode.SaveFile Then Return False
            If _browser Is Nothing Then Return False
            If _browser.Provider Is Nothing Then Return False
            If String.IsNullOrWhiteSpace(_browser.CurrentPath) Then Return False

            Dim safeFileName As String = If(_saveFileName, String.Empty).Trim()
            If Not IsValidSaveFileName(safeFileName) Then Return False

            Return True
        End Function

        Private Shared Function IsValidSaveFileName(fileName As String) As Boolean
            Dim candidate As String = If(fileName, String.Empty).Trim()

            If candidate.Length = 0 Then Return False
            If String.Equals(candidate, ".", StringComparison.Ordinal) Then Return False
            If String.Equals(candidate, "..", StringComparison.Ordinal) Then Return False
            If candidate.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0 Then Return False

            Return True
        End Function

        Private Function CanAcceptCurrentPath() As Boolean
            If _pickerPolicy Is Nothing Then Return False
            If Not _pickerPolicy.AcceptCurrentPathWhenNoSelection Then Return False

            If _browser Is Nothing Then Return False
            If String.IsNullOrWhiteSpace(_browser.CurrentPath) Then Return False
            If _browser.Provider Is Nothing Then Return False

            If Not _browser.Provider.DirectoryExists(_browser.CurrentPath) Then Return False

            Dim synthetic As New FileSystemEntry With {
                .Name = _browser.CurrentPath,
                .FullPath = _browser.CurrentPath,
                .EntryType = InferCurrentPathEntryType(_browser.CurrentPath)
            }

            Return _pickerPolicy.CanReturn(synthetic)
        End Function

        Private Shared Function InferCurrentPathEntryType(path As String) As FileSystemEntryType
            If String.IsNullOrWhiteSpace(path) Then Return FileSystemEntryType.Folder

            Dim trimmed As String = path.Trim()

            If trimmed.Length >= 2 AndAlso trimmed(1) = ":"c Then
                If trimmed.EndsWith("\", StringComparison.Ordinal) OrElse
                   trimmed.EndsWith("/", StringComparison.Ordinal) Then

                    Return FileSystemEntryType.Drive
                End If
            End If

            Return FileSystemEntryType.Folder
        End Function

        Private Shared Function CreateBrowserSelectionPolicy(options As MASFilePickerOptions) As MASFileBrowserSelectionPolicy
            Dim safeOptions As MASFilePickerOptions = MASFilePickerOptions.SafeClone(options)
            safeOptions.Normalize()

            Select Case safeOptions.Mode
                Case MASFilePickerMode.OpenFile
                    Return MASFileBrowserSelectionPolicy.FilesOnly(safeOptions.AllowMultiSelect)

                Case MASFilePickerMode.SaveFile
                    Return MASFileBrowserSelectionPolicy.FilesOnly(False)

                Case MASFilePickerMode.PickTargets
                    Dim kinds As MASFileSystemSelectionKinds = safeOptions.TargetKinds

                    If kinds = MASFileSystemSelectionKinds.None Then
                        kinds = MASFileSystemSelectionKinds.Folder Or MASFileSystemSelectionKinds.Drive
                    End If

                    Return New MASFileBrowserSelectionPolicy() With {
                .AllowMultiSelect = safeOptions.AllowMultiSelect,
                .AllowFileSelection = (kinds And MASFileSystemSelectionKinds.FileItem) = MASFileSystemSelectionKinds.FileItem,
                .AllowFolderSelection = (kinds And MASFileSystemSelectionKinds.Folder) = MASFileSystemSelectionKinds.Folder,
                .AllowDriveSelection = (kinds And MASFileSystemSelectionKinds.Drive) = MASFileSystemSelectionKinds.Drive,
                .AllowSpecialSelection = (kinds And MASFileSystemSelectionKinds.Special) = MASFileSystemSelectionKinds.Special
            }

                Case Else
                    Return MASFileBrowserSelectionPolicy.FilesOnly(False)
            End Select
        End Function

        Private Sub WireEvents()
            If _eventsWired Then Return
            If _browser Is Nothing Then Return

            AddHandler _browser.SelectionChanged, AddressOf OnBrowserSelectionChanged
            AddHandler _browser.CurrentPathChanged, AddressOf OnBrowserCurrentPathChanged
            AddHandler _browser.ItemActivated, AddressOf OnBrowserItemActivated

            _eventsWired = True
        End Sub

        Private Sub UnwireEvents()
            If Not _eventsWired Then Return
            If _browser Is Nothing Then Return

            Try
                RemoveHandler _browser.SelectionChanged, AddressOf OnBrowserSelectionChanged
            Catch masCaughtException2 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDisposeCleanup(masCaughtException2)
            End Try

            Try
                RemoveHandler _browser.CurrentPathChanged, AddressOf OnBrowserCurrentPathChanged
            Catch masCaughtException3 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDisposeCleanup(masCaughtException3)
            End Try

            Try
                RemoveHandler _browser.ItemActivated, AddressOf OnBrowserItemActivated
            Catch masCaughtException4 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDisposeCleanup(masCaughtException4)
            End Try

            _eventsWired = False
        End Sub

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

            If _options.Mode = MASFilePickerMode.SaveFile Then
                Dim item As FileSystemEntry = _browser.SelectedItem

                If item IsNot Nothing AndAlso item.IsFile Then
                    _saveFileName = item.Name
                End If
            End If

            RaiseStateChanged()
        End Sub

        Private Sub OnBrowserCurrentPathChanged(sender As Object, e As EventArgs)
            If _disposed Then Return
            RaiseStateChanged()
        End Sub

        Private Sub OnBrowserItemActivated(sender As Object, e As FileSystemEntryEventArgs)
            If _disposed Then Return
            If e Is Nothing OrElse e.Entry Is Nothing Then Return

            Dim entry As FileSystemEntry = e.Entry

            If _pickerPolicy IsNot Nothing AndAlso _pickerPolicy.CanNavigateInto(entry) Then
                NavigateTo(entry.FullPath)
                Return
            End If

            Select Case _options.Mode
                Case MASFilePickerMode.OpenFile
                    If entry.IsFile AndAlso _pickerPolicy IsNot Nothing AndAlso _pickerPolicy.CanReturn(entry) Then
                        Dim result As MASFilePickerResult = MASFilePickerResult.FromPath(entry.FullPath)

                        If result IsNot Nothing AndAlso result.Accepted Then
                            RaiseEvent Accepted(Me, New MASFilePickerResultEventArgs(result))
                        End If
                    End If

                Case MASFilePickerMode.SaveFile
                    If entry.IsFile Then
                        SaveFileName = entry.Name
                    End If

                Case MASFilePickerMode.PickTargets
                    If _pickerPolicy IsNot Nothing AndAlso _pickerPolicy.CanReturn(entry) Then
                        Dim result As MASFilePickerResult = MASFilePickerResult.FromPath(entry.FullPath)

                        If result IsNot Nothing AndAlso result.Accepted Then
                            RaiseEvent Accepted(Me, New MASFilePickerResultEventArgs(result))
                        End If
                    End If
            End Select
        End Sub

        Private Sub RaiseStateChanged()
            RaiseEvent StateChanged(Me, EventArgs.Empty)
        End Sub

        Private Sub ThrowIfDisposed()
            If _disposed Then Throw New ObjectDisposedException(NameOf(FilePickerController))
        End Sub

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

            _disposed = True

            UnwireEvents()

            Try
                If _browser IsNot Nothing Then
                    _browser.Dispose()
                End If
            Catch masCaughtException5 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDisposeCleanup(masCaughtException5)
            End Try
        End Sub

    End Class

End Namespace