Imports FirebirdSql.Data.FirebirdClient
Imports System.Text.RegularExpressions
Imports DigitalData.Modules.Logging
Imports System.ComponentModel

''' <summary>
''' MODULE:         Firebird
''' 
''' VERSION:        0.0.0.4
''' 
''' DATE:           18.12.2018
''' 
''' DESCRIPTION:    
'''                    
''' DEPENDENCIES:   NLog, >= 4.5.10
''' 
'''                 EntityFramework.Firebird, >= 6.4.0
'''                 
'''                 FirebirdSql.Data.FirebirdClient, >= 6.4.0
''' 
''' PARAMETERS:     LogConfig, DigitalData.Modules.Logging.LogConfig
'''                 The LogFactory containing the current log config. Used to instanciate the class logger for this and any dependent class
'''                 
'''                 DataSource, String
'''                 The server where the database lives, for example 127.0.0.1 or dd-vmx09-vm03
'''                 
'''                 Database, String                
'''                 The location of the Database in the format `127.0.0.1:E:\Path\To\Database.FDB`
'''                 
'''                 User, String
'''                 The user name to connect as
'''                 
'''                 Password, String
'''                 The user's password
'''                 
''' PROPERTIES:     ConnectionEstablished, Boolean
'''                 If the last opened connection was successful
''' 
'''                 ConnectionFailed, Boolean
'''                 If the last opened connection failed
'''                 
'''                 ConnectionString, String
'''                 The used connectionstring
'''                 
''' EXAMPLES:       
'''                 
''' REMARKS:        If the connection fails due to "wrong username or password", the cause might be that the server harddrive is full..
''' </summary>
Public Class Firebird
    Private _Logger As Logger
    Private _LogConfig As LogConfig
    Private _connectionServer As String
    Private _connectionDatabase As String
    Private _connectionUsername As String
    Private _connectionPassword As String
    Private _connectionString As String
    Public _DBInitialized As Boolean = False

    Public Const MAX_POOL_SIZE = 1000

    Public Enum TransactionMode
        <Description("Use no transaction, neither internal nor external")>
        NoTransaction
        <Description("Use the transaction supplied in the Transaction Parameter")>
        ExternalTransaction
        <Description("Create an internal transaction on the fly")>
        WithTransaction
    End Enum

    Public ReadOnly Property ConnectionString As String
        Get
            Return _connectionString
        End Get
    End Property

    Public ReadOnly Property DatabaseName As String
        Get
            Dim oRegex As New Regex("^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:")
            Dim oPath As String = oRegex.Replace(_connectionDatabase, "")
            Dim oFileInfo As New IO.FileInfo(oPath)
            Return oFileInfo.Name
        End Get
    End Property

    ''' <summary>
    ''' 
    ''' </summary>
    ''' <param name="LogConfig">The LogFactory containing the current log config. Used to instanciate the class logger for this and any dependent class</param>
    ''' <param name="Datasource">The server where the database lives, for example 127.0.0.1 or dd-vmx09-vm03</param>
    ''' <param name="Database">The location of the Database in the format `127.0.0.1:E:\Path\To\Database.FDB`</param>
    ''' <param name="User">The user name to connect as</param>
    ''' <param name="Password">The user's password</param>
    ''' <exception cref="Exceptions.DatabaseException"></exception>
    Public Sub New(LogConfig As LogConfig, Datasource As String, Database As String, User As String, Password As String)
        Try
            _LogConfig = LogConfig
            _Logger = _LogConfig.GetLogger()
            Dim oConnectionString = GetConnectionString(Datasource, Database, User, Password)

            _connectionServer = Datasource
            _connectionDatabase = Database
            _connectionUsername = User
            _connectionPassword = Password
            _connectionString = oConnectionString

            _Logger.Debug("Connecting to database..")

            ' Test the connection
            Dim oConnection = GetConnection()
            ' If initial connection was successfully, close it
            oConnection.Close()

            If oConnection Is Nothing Then
                Throw New Exceptions.DatabaseException()
            Else
                _DBInitialized = True
            End If

            _Logger.Debug("Connection sucessfully established!")
        Catch ex As Exception
            _Logger.Error(ex)
        End Try

    End Sub

    Public Function GetConnection() As FbConnection
        Try
            Dim oConnection = New FbConnection(_connectionString)
            oConnection.Open()

            Return oConnection
        Catch ex As Exception
            _Logger.Error(ex)
            Return Nothing
        End Try
    End Function

    ''' <summary>
    ''' Builds a connectionstring from the provided arguments.
    ''' </summary>
    ''' <param name="DataSource">The database server where to connect to</param>
    ''' <param name="Database">The datasource, eg. the path of the FDB-file</param>
    ''' <param name="User">The user used to connect to the database</param>
    ''' <param name="Password">The password of the connecting user</param>
    ''' <returns>A connectionstring</returns>
    Private Function GetConnectionString(DataSource As String, Database As String, User As String, Password As String) As String
        Return New FbConnectionStringBuilder With {
            .DataSource = DataSource,
            .Database = Database,
            .UserID = User,
            .Password = Password,
            .Charset = "UTF8",
            .MaxPoolSize = MAX_POOL_SIZE
        }.ToString()
    End Function

    Private Function MaybeGetTransaction(Connection As FbConnection, Mode As TransactionMode, Transaction As FbTransaction) As FbTransaction
        If Mode = TransactionMode.NoTransaction Then
            Return Nothing
        ElseIf Mode = TransactionMode.ExternalTransaction Then
            Return Transaction
        Else
            Return Connection.BeginTransaction()
        End If
    End Function

    Private Function MaybeCommitTransaction(Transaction As FbTransaction, TransactionMode As TransactionMode) As Boolean
        Select Case TransactionMode
            Case TransactionMode.NoTransaction
                Return True
            Case TransactionMode.ExternalTransaction
                Return True
            Case TransactionMode.WithTransaction
                Try
                    Transaction.Commit()
                    Return True
                Catch ex As Exception
                    _Logger.Error(ex)
                    Return False
                End Try
            Case Else
                Return True
        End Select
    End Function

    ''' <summary>
    ''' Executes a non-query command.
    ''' </summary>
    ''' <param name="SqlCommand">The command to execute</param>
    ''' <param name="Connection">The Firebird connection to use</param>
    ''' <returns>True, if command was executed sucessfully. Otherwise false.</returns>
    Public Function ExecuteNonQueryWithConnection(SqlCommand As String, Connection As FbConnection, Optional TransactionMode As TransactionMode = TransactionMode.WithTransaction, Optional Transaction As FbTransaction = Nothing) As Boolean
        _Logger.Debug("Executing Non-Query: {0}", SqlCommand)

        If Connection Is Nothing Then
            _Logger.Warn("Connection is nothing!")
            Return Nothing
        End If

        Dim oTransaction = MaybeGetTransaction(Connection, TransactionMode, Transaction)

        Try
            Dim oCommand As New FbCommand With {
                .CommandText = SqlCommand,
                .Connection = Connection
            }

            If Not IsNothing(oTransaction) Then
                oCommand.Transaction = oTransaction
            End If

            oCommand.ExecuteNonQuery()
            _Logger.Debug("Command executed!")
        Catch ex As Exception
            _Logger.Error(ex, $"Error in ExecuteNonQuery while executing command: [{SqlCommand}]")
            _Logger.Warn($"Unexpected error in ExecuteNonQueryWithConnection: [{SqlCommand}]")
            Throw ex
        Finally
            MaybeCommitTransaction(oTransaction, TransactionMode)
        End Try

        Return True
    End Function

    ''' <summary>
    ''' Executes a non-query command.
    ''' </summary>
    ''' <param name="SqlCommand">The command to execute</param>
    ''' <returns>True, if command was executed sucessfully. Otherwise false.</returns>
    Public Function ExecuteNonQuery(SqlCommand As String) As Boolean
        Using oConnection As FbConnection = GetConnection()
            Return ExecuteNonQueryWithConnection(SqlCommand, oConnection)
        End Using
    End Function

    ''' <summary>
    ''' Executes a non-query command inside the specified transaction.
    ''' </summary>
    ''' <param name="SqlCommand">The command to execute</param>
    ''' <returns>True, if command was executed sucessfully. Otherwise false.</returns>
    Public Function ExecuteNonQuery(SqlCommand As String, Transaction As FbTransaction) As Boolean
        Using oConnection As FbConnection = GetConnection()
            Return ExecuteNonQueryWithConnection(SqlCommand, oConnection, TransactionMode.ExternalTransaction, Transaction)
        End Using
    End Function

    ''' <summary>
    ''' Executes a sql query resulting in a scalar value.
    ''' </summary>
    ''' <param name="SqlQuery">The query to execute</param>
    ''' <param name="Connection">The Firebird connection to use</param>
    ''' <returns>The scalar value if the command was executed successfully. Nothing otherwise.</returns>
    Public Function GetScalarValueWithConnection(SqlQuery As String, Connection As FbConnection, Optional TransactionMode As TransactionMode = TransactionMode.WithTransaction, Optional Transaction As FbTransaction = Nothing) As Object
        _Logger.Debug("Fetching Scalar-Value: {0}", SqlQuery)

        If Connection Is Nothing Then
            _Logger.Warn("Connection is nothing!")
            Return Nothing
        End If

        Dim oTransaction = MaybeGetTransaction(Connection, TransactionMode, Transaction)
        Dim oResult As Object

        Try
            Dim oCommand As New FbCommand With {
                .CommandText = SqlQuery,
                .Connection = Connection,
                .Transaction = oTransaction
            }
            oResult = oCommand.ExecuteScalar()
        Catch ex As Exception
            _Logger.Error(ex, $"Error in ReturnScalar while executing command: [{SqlQuery}]")
            Throw ex
        Finally
            MaybeCommitTransaction(oTransaction, TransactionMode)
        End Try

        Return oResult
    End Function

    ''' <summary>
    ''' Executes a sql query resulting in a scalar value.
    ''' </summary>
    ''' <param name="SqlQuery">The query to execute</param>
    ''' <returns>The scalar value if the command was executed successfully. Nothing otherwise.</returns>
    Public Function GetScalarValue(SqlQuery As String) As Object
        Dim oConnection As FbConnection = GetConnection()
        Dim oScalarValue As Object = GetScalarValueWithConnection(SqlQuery, oConnection)
        oConnection.Close()

        Return oScalarValue
    End Function

    ''' <summary>
    ''' Executes a sql query resulting in a table of values.
    ''' </summary>
    ''' <param name="SqlQuery">The query to execute</param>
    ''' <param name="Connection">The Firebird connection to use</param>
    ''' <returns>A datatable containing the results if the command was executed successfully. Nothing otherwise.</returns>
    Public Function GetDatatableWithConnection(SqlQuery As String, Connection As FbConnection, Optional TransactionMode As TransactionMode = TransactionMode.NoTransaction, Optional Transaction As FbTransaction = Nothing) As DataTable
        _Logger.Debug("Fetching Datatable: {0}", SqlQuery)

        If Connection Is Nothing Then
            _Logger.Warn("Connection is nothing!")
            Return Nothing
        End If

        Dim oTransaction = MaybeGetTransaction(Connection, TransactionMode, Transaction)
        Dim oDatatable As New DataTable() With {
            .TableName = "DDRESULT"
        }

        Try
            Dim oAdapter As New FbDataAdapter(New FbCommand With {
                .CommandText = SqlQuery,
                .Connection = Connection,
                .Transaction = oTransaction
            })

            oAdapter.Fill(oDatatable)
        Catch ex As Exception
            _Logger.Error(ex)
            _Logger.Warn("Error in GetDatatableWithConnection while executing command: [{0}]", SqlQuery)
            Throw ex
        Finally
            MaybeCommitTransaction(oTransaction, TransactionMode)
        End Try

        Return oDatatable
    End Function

    ''' <summary>
    ''' Executes a sql query resulting in a table of values.
    ''' </summary>
    ''' <param name="SqlQuery">The query to execute</param>
    ''' <returns>A datatable containing the results if the command was executed successfully. Nothing otherwise.</returns>
    Public Function GetDatatable(SqlQuery As String, Optional TransactionMode As TransactionMode = TransactionMode.NoTransaction, Optional Transaction As FbTransaction = Nothing) As DataTable
        Try
            Dim oConnection As FbConnection = GetConnection()
            Dim oDatatable As DataTable = GetDatatableWithConnection(SqlQuery, oConnection, TransactionMode, Transaction)
            oConnection.Close()

            Return oDatatable
        Catch ex As Exception
            _Logger.Error(ex)
            _Logger.Warn("Error in GetDatatable while executing command: '{0}'", SqlQuery)
            Throw ex
        End Try
    End Function
End Class
