Imports System.IO
Imports System.Reflection
Imports System.Xml.Serialization
Imports DigitalData.Modules.Logging
Imports DigitalData.Modules.Config.ConfigAttributes
Imports DigitalData.Modules.Base

Public Class ConfigManager(Of T)
    Public Const USER_CONFIG_NAME As String = "UserConfig.xml"
    Public Const COMPUTER_CONFIG_NAME As String = "ComputerConfig.xml"
    Public Const APP_CONFIG_NAME As String = "AppConfig.xml"

    Private ReadOnly _LogConfig As LogConfig
    Private ReadOnly _Logger As Logger
    Private ReadOnly _File As FilesystemEx

    Private ReadOnly _UserDirectory As String
    Private ReadOnly _UserConfigPath As String
    Private ReadOnly _ComputerDirectory As String
    Private ReadOnly _ComputerConfigPath As String
    Private ReadOnly _AppConfigDirectory As String
    Private ReadOnly _AppConfigPath As String

    Private ReadOnly _TestMode As Boolean = False

    Private ReadOnly _Blueprint As T
    Private ReadOnly _BlueprintType As Type
    Private ReadOnly _Serializer As XmlSerializer

    Private ReadOnly _ExcludedAttributes = New List(Of Type) From {
        GetType(ConnectionStringAttribute),
        GetType(ConnectionStringAppServerAttribute),
        GetType(ConnectionStringTestAttribute),
        GetType(EDMIAppServerAttribute),
        GetType(GlobalSettingAttribute)
    }

    Private ReadOnly _ConnectionStringAttributes = New List(Of Type) From {
        GetType(ConnectionStringAttribute),
        GetType(ConnectionStringAppServerAttribute),
        GetType(ConnectionStringTestAttribute)
    }

    ''' <summary>
    ''' Signals that all properties will be written to (and read from) the UserConfig.xml
    ''' 
    ''' If Value is `True`:
    ''' - AppConfig.xml does NOT exist
    ''' - ComputerConfig.xml does NOT exist
    ''' - ConnectionStrings will be saved to or read from UserConfig.xml
    ''' 
    ''' If Value is `False`:
    ''' - No ConnectionStrings will be saved to or read from UserConfig.xml
    ''' 
    ''' Can be overwritten by optional parameter `ForceUserConfig`
    ''' </summary>
    Private _WriteAllValuesToUserConfig As Boolean = False

    ''' <summary>
    ''' Returns the currently loaded config object
    ''' </summary>
    ''' <returns></returns>
    Public ReadOnly Property Config As T

    ''' <summary>
    ''' Path to the current user config.
    ''' </summary>
    ''' <returns></returns>
    Public ReadOnly Property UserConfigPath As String
        Get
            Return _UserConfigPath
        End Get
    End Property

    ''' <summary>
    ''' Path to the current computer config.
    ''' </summary>
    ''' <returns></returns>
    Public ReadOnly Property ComputerConfigPath As String
        Get
            Return _ComputerConfigPath
        End Get
    End Property

    ''' <summary>
    ''' Path to the current Application config.
    ''' </summary>
    ''' <returns></returns>
    Public ReadOnly Property AppConfigPath As String
        Get
            Return _AppConfigPath
        End Get
    End Property

    ''' <summary>
    ''' Creates a new instance of the ConfigManager
    ''' </summary>
    ''' <seealso cref="ConfigSample"/>
    ''' <param name="LogConfig">LogConfig instance</param>
    ''' <param name="UserConfigPath">The path to check for a user config file, eg. AppData (Usually Application.UserAppDataPath or Application.LocalUserAppDataPath)</param>
    ''' <param name="ComputerConfigPath">The path to check for a computer config file, eg. ProgramData (Usually Application.CommonAppDataPath)</param>
    ''' <param name="ApplicationStartupPath">The path to check for a third config file. This is useful when running the Application in an environment where AppData/ProgramData directories are not available</param>
    ''' <param name="ForceUserConfig">Override values from ComputerConfig with UserConfig</param>
    Public Sub New(LogConfig As LogConfig, UserConfigPath As String, ComputerConfigPath As String, Optional ApplicationStartupPath As String = "", Optional ForceUserConfig As Boolean = False)
        _LogConfig = LogConfig
        _Logger = LogConfig.GetLogger()
        _File = New FilesystemEx(_LogConfig)

        _Blueprint = Activator.CreateInstance(Of T)
        _BlueprintType = _Blueprint.GetType
        _Serializer = New XmlSerializer(_BlueprintType)

        _UserDirectory = _File.CreateDirectory(UserConfigPath)
        _UserConfigPath = Path.Combine(_UserDirectory, USER_CONFIG_NAME)

        If ComputerConfigPath <> String.Empty Then
            If IO.File.Exists(ComputerConfigPath) Then
                _ComputerDirectory = _File.CreateDirectory(ComputerConfigPath, False)
            Else
                _ComputerDirectory = ComputerConfigPath
            End If
            _ComputerConfigPath = Path.Combine(_ComputerDirectory, COMPUTER_CONFIG_NAME)
        End If

        If ApplicationStartupPath <> String.Empty Then
            _AppConfigPath = Path.Combine(ApplicationStartupPath, APP_CONFIG_NAME)

        End If

        _WriteAllValuesToUserConfig = ForceUserConfig

        _Config = LoadConfig()
    End Sub

    ''' <summary>
    ''' Creates a new ConfigManager with a single (user)config path
    ''' </summary>
    ''' <param name="LogConfig">LogConfig instance</param>
    ''' <param name="ConfigPath">The path to check for a user config file, eg. AppData (Usually Application.UserAppDataPath or Application.LocalUserAppDataPath)</param>
    Public Sub New(LogConfig As LogConfig, ConfigPath As String)
        MyClass.New(LogConfig, ConfigPath, String.Empty, String.Empty, ForceUserConfig:=True)
    End Sub

    ''' <summary>
    ''' Save the current config object to `UserConfigPath`
    ''' </summary>
    ''' <param name="ForceAll">Force saving all attributes including the attributes marked as excluded</param>
    ''' <returns>True if save was successful, False otherwise</returns>
    Public Function Save(Optional ForceAll As Boolean = False) As Boolean
        Try
            WriteToFile(Config, _UserConfigPath, ForceAll)
            Return True
        Catch ex As Exception
            _Logger.Error(ex)
            Return False
        End Try
    End Function

    ''' <summary>
    ''' Reloads the config object from file.
    ''' </summary>
    ''' <returns>True if reload was successful, False otherwise</returns>
    Public Function Reload() As Boolean
        Try
            _Config = LoadConfig()
            Return True
        Catch ex As Exception
            _Logger.Error(ex)
            Return False
        End Try
    End Function

    ''' <summary>
    ''' Copies all properties from Source to Target, except those who have an attribute
    ''' listed in ExcludedAttributeTypes
    ''' </summary>
    ''' <param name="Source">Source config object</param>
    ''' <param name="Target">Target config object</param>
    ''' <param name="ExcludedAttributeTypes">List of Attribute type to exclude</param>
    Private Sub CopyValues(Source As T, Target As T, Optional ExcludedAttributeTypes As List(Of Type) = Nothing)
        Dim oType As Type = GetType(T)
        Dim oExcludedAttributeTypes = IIf(IsNothing(ExcludedAttributeTypes), New List(Of Type), ExcludedAttributeTypes)
        Dim oProperties = oType.GetProperties().
            Where(Function(p) p.CanRead And p.CanWrite).
            Where(Function(p)
                      For Each oAttributeType As Type In oExcludedAttributeTypes
                          If Attribute.IsDefined(p, oAttributeType) Then
                              Return False
                          End If
                      Next
                      Return True
                  End Function)

        For Each oProperty As PropertyInfo In oProperties
            ' TODO: Process individual Subfields of class-objects
            ' to allow for the PasswordAttribute to be set on class properies aka nested properties

            Dim oValue = oProperty.GetValue(Source, Nothing)
            If Not IsNothing(oValue) Then
                oProperty.SetValue(Target, oValue, Nothing)
            End If
        Next
    End Sub

    ''' <summary>
    ''' Filters a config object by copying all values except `ExcludedAttributeTypes`
    ''' </summary>
    ''' <param name="Data">Config object</param>
    ''' <param name="ExcludedAttributeTypes">List of Attribute type to exclude</param>
    ''' <returns></returns>
    Private Function FilterValues(ByVal Data As T, ExcludedAttributeTypes As List(Of Type)) As T
        Dim oResult As T = Activator.CreateInstance(Of T)

        CopyValues(Data, oResult, ExcludedAttributeTypes)
        Return oResult
    End Function

    Private Function LoadConfig() As T
        ' first create an empty/default config object
        Dim oConfig As T = Activator.CreateInstance(_BlueprintType)

        ' try to load the special app config
        oConfig = LoadAppConfig(oConfig)

        ' try to load the computer config
        oConfig = LoadComputerConfig(oConfig)

        ' now try to load userconfig
        oConfig = LoadUserConfig(oConfig)
        Return oConfig
    End Function

    Private Function LoadAppConfig(ByVal Config As T) As T
        If Not String.IsNullOrEmpty(_AppConfigPath) AndAlso File.Exists(_AppConfigPath) Then
            Try
                Dim oAppConfig = ReadFromFile(_AppConfigPath)
                CopyValues(oAppConfig, Config)

                _Logger.Info("AppConfig exists and will be used. [{0}]", _AppConfigPath)
            Catch ex As Exception
                _Logger.Error(ex)
                _Logger.Warn("ApplicationConfig could not be loaded!")
            End Try

            _WriteAllValuesToUserConfig = False
        Else
            _Logger.Debug("ApplicationConfig does not exist.")
            _WriteAllValuesToUserConfig = True
        End If

        Return Config
    End Function

    Private Function LoadComputerConfig(ByVal Config As T) As T
        If _WriteAllValuesToUserConfig = False Then
            _Logger.Info("AppConfig exists. ComputerConfig will NOT be used")
        ElseIf File.Exists(_ComputerConfigPath) Then
            Try
                Dim oComputerConfig = ReadFromFile(_ComputerConfigPath)
                CopyValues(oComputerConfig, Config)

                _Logger.Info("ComputerConfig exists and will be used. [{0}]", _ComputerConfigPath)
            Catch ex As Exception
                _Logger.Error(ex)
                _Logger.Warn("Computer config could not be loaded!")
            End Try
            _WriteAllValuesToUserConfig = False
        Else
            _Logger.Debug("Computer config does not exist.")
            _WriteAllValuesToUserConfig = True
        End If

        Return Config
    End Function

    Private Function LoadUserConfig(ByVal Config As T) As T
        If File.Exists(_UserConfigPath) Then
            Try
                Dim oUserConfig = ReadFromFile(_UserConfigPath)
                _Logger.Debug("UserConfig exists and will be used. [{0}]", _UserConfigPath)

                ' if user config exists
                If Not IsNothing(oUserConfig) Then
                    ' Copy values from user config to final config
                    If _WriteAllValuesToUserConfig Then
                        CopyValues(oUserConfig, Config, New List(Of Type))
                    Else
                        CopyValues(oUserConfig, Config, _ExcludedAttributes)
                    End If
                End If
            Catch ex As Exception
                _Logger.Error(ex)
                _Logger.Warn("User config could not be loaded!")
            End Try
        Else
            _Logger.Debug("User config does not exist. Default config will be created")
            WriteToFile(Config, _UserConfigPath, False)
        End If

        Return Config
    End Function

    Private Function TestHasAttribute(Config As T, AttributeType As Type) As Boolean
        For Each oProperty As PropertyInfo In Config.GetType.GetProperties()
            If Attribute.IsDefined(oProperty, GetType(ConnectionStringAttribute)) Then
                Return True
            End If
        Next

        Return False
    End Function

    ''' <summary>
    ''' Serialize a config object to byte array
    ''' </summary>
    ''' <param name="Data"></param>
    ''' <returns></returns>
    Private Function Serialize(Data As T) As Byte()
        Try
            _Logger.Debug("Serializing config object")

            Using oStream = New MemoryStream()
                _Serializer.Serialize(oStream, Data)
                _Logger.Debug("Object serialized.")
                Return oStream.ToArray()
            End Using
        Catch ex As Exception
            _Logger.Error(ex)
            Throw ex
        End Try
    End Function

    ''' <summary>
    ''' Write an object to disk as xml
    ''' </summary>
    ''' <param name="Data">The object to write</param>
    ''' <param name="Path">The file name to write to</param>
    Private Sub WriteToFile(Data As T, Path As String, ForceAll As Boolean)
        Try
            _Logger.Debug("Saving config to: {0}", Path)

            ' If config was loaded from computer config, 
            ' DO NOT save connection string, etc. to user config
            If _WriteAllValuesToUserConfig = False And ForceAll = False Then
                Data = FilterValues(Data, _ExcludedAttributes)
            End If

            Dim oBytes = Serialize(Data)

            Using oFileStream = New FileStream(Path, FileMode.Create, FileAccess.Write)
                oFileStream.Write(oBytes, 0, oBytes.Length)
                oFileStream.Flush()
            End Using
        Catch ex As Exception
            _Logger.Warn("Could not save config to {0}", Path)
            _Logger.Error(ex)
            Throw ex
        End Try
    End Sub

    ''' <summary>
    ''' Reads an xml from disk and deserializes to object
    ''' </summary>
    ''' <returns></returns>
    Private Function ReadFromFile(Path As String) As T
        Try
            _Logger.Debug("Loading config from: {0}", Path)
            Dim oConfig As T

            Using oReader As New StreamReader(Path)
                oConfig = _Serializer.Deserialize(oReader)
            End Using

            ' If oConfig is Nothing, a config file was created but nothing was written to it.
            ' In this case we need to create oConfig from defaults so we have at least some config object
            If oConfig Is Nothing Then
                _Logger.Debug("Config file is valid but empty. Loading default values")
                oConfig = Activator.CreateInstance(_BlueprintType)
            End If

            Return oConfig
        Catch ex As Exception
            _Logger.Warn("Could not load config from {0}", Path)
            _Logger.Error(ex)
            Throw ex
        End Try
    End Function
End Class
