VERSION 1.0 CLASS
BEGIN
  MultiUse = -1  'True
END
Attribute VB_Name = "ValidationRuleEngine"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit

Private pRules As VBA.Collection

Private Sub Class_Initialize()
    Set pRules = New VBA.Collection
End Sub

' ============================================================
' Add a Required rule directly (convenience method).
' ============================================================
Public Sub AddRequired(ByVal colIndex As String)
    Dim r As ValidationRule: Set r = New ValidationRule
    r.RuleKind = ValRule_Required
    r.ColIndex = IIf(IsNumeric(colIndex), colIndex, ColNum(CStr(colIndex)))
    pRules.Add r
    Set r = Nothing
End Sub

Public Sub AddDate(colIndex As Variant)
    Dim r As ValidationRule: Set r = New ValidationRule
    r.RuleKind = ValRule_Date
    r.ColIndex = IIf(IsNumeric(colIndex), colIndex, ColNum(CStr(colIndex)))
    pRules.Add r
    Set r = Nothing
End Sub

Public Sub AddNumber(colIndex As Variant, Optional totalDigits As Long = 0, Optional decimalDigits As Long = 0)
    Dim r As ValidationRule: Set r = New ValidationRule
    r.RuleKind = ValRule_Number
    r.ColIndex = IIf(IsNumeric(colIndex), colIndex, ColNum(CStr(colIndex)))
    If totalDigits > 0 Then r.NumberDigits = totalDigits
    If decimalDigits > 0 Then r.NumberDec = decimalDigits
    pRules.Add r
    Set r = Nothing
End Sub

Public Sub AddCodeSelect(colIndex As Variant, cacheName As String)
    Dim r As ValidationRule: Set r = New ValidationRule
    r.RuleKind = ValRule_CodeSelect
    r.ColIndex = IIf(IsNumeric(colIndex), colIndex, ColNum(CStr(colIndex)))
    r.CacheName = cacheName
    pRules.Add r
    Set r = Nothing
End Sub

Public Sub AddRange(colIndex As Variant, minVal As Double, maxVal As Double)
    Dim r As ValidationRule: Set r = New ValidationRule
    r.RuleKind = ValRule_Range
    r.ColIndex = IIf(IsNumeric(colIndex), colIndex, ColNum(CStr(colIndex)))
    r.MinVal = minVal
    r.MaxVal = maxVal
    pRules.Add r
    Set r = Nothing
End Sub

Public Sub AddDuplicate(colIndex As Variant, Optional firstRow As Long = 0)
    Dim r As ValidationRule: Set r = New ValidationRule
    r.RuleKind = ValRule_Duplicate
    r.ColIndex = IIf(IsNumeric(colIndex), colIndex, ColNum(CStr(colIndex)))
    If firstRow > 0 Then r.StartRow = firstRow
    pRules.Add r
    Set r = Nothing
End Sub

Public Sub AddChar(colIndex As Variant, charLen As Long)
    Dim r As ValidationRule: Set r = New ValidationRule
    r.RuleKind = ValRule_Char
    r.ColIndex = IIf(IsNumeric(colIndex), colIndex, ColNum(CStr(colIndex)))
    r.CharLen = charLen
    pRules.Add r
    Set r = Nothing
End Sub

Public Sub AddVarchar(colIndex As Variant, maxLen As Long)
    Dim r As ValidationRule: Set r = New ValidationRule
    r.RuleKind = ValRule_Varchar
    r.ColIndex = IIf(IsNumeric(colIndex), colIndex, ColNum(CStr(colIndex)))
    r.VarcharLen = maxLen
    pRules.Add r
    Set r = Nothing
End Sub

Public Sub AddCheck01(colIndex As Variant)
    Dim r As ValidationRule: Set r = New ValidationRule
    r.RuleKind = ValRule_Check01
    r.ColIndex = IIf(IsNumeric(colIndex), colIndex, ColNum(CStr(colIndex)))
    pRules.Add r
    Set r = Nothing
End Sub



Public Sub AddAlphanumeric(colIndex As Variant)
    Dim r As ValidationRule: Set r = New ValidationRule
    r.RuleKind = ValRule_Alphanumeric
    r.ColIndex = IIf(IsNumeric(colIndex), colIndex, ColNum(CStr(colIndex)))
    pRules.Add r
    Set r = Nothing
End Sub

' ============================================================
' Run all rules against the given row.
' Clears row background at start, outputs error on failure.
' lastDataRow is required when Duplicate rules are registered.
' Returns Nothing if all rules pass.
' ============================================================
Public Function ValidateRow(ws As Worksheet, rowNum As Long, Optional lastDataRow As Long = 0) As ValidationResult
    ' Clear row background
    Call ClearRowBg(ws, rowNum)

    Dim r As ValidationRule
    Dim result As ValidationResult

    For Each r In pRules
        Set result = r.ValidateRow(ws, rowNum, lastDataRow)
        If Not result.Passed Then
            Call OutputError(ws, rowNum, result)
            Set ValidateRow = result
            Exit Function
        End If
    Next r

    ' All passed
    Set ValidateRow = New ValidationResult
End Function

' ============================================================
' Number of rules registered.
' ============================================================
Public Property Get RuleCount() As Long
    RuleCount = pRules.Count
End Property

' ============================================================
' Clear background color for the given row.
' Uses the colIndex of each registered rule to build the range.
' Call this before running ValidateRow.
' ============================================================
Public Sub ClearRowBg(ws As Worksheet, rowNum As Long)
    Dim sheetConfDict As Object: Set sheetConfDict = GetSheetConfig()
    Dim sheetConf As Object: Set sheetConf = sheetConfDict(ws.CodeName)
    Dim errorCol As Long: errorCol = ColNum(CStr(sheetConf("ErrorCol")))
    Dim endCol As Long: endCol = ColNum(CStr(sheetConf("EndCol")))

    Dim clearRange As Range
    Set clearRange = ws.Range(ws.Cells(rowNum, errorCol), ws.Cells(rowNum, endCol))
    clearRange.Interior.Color = vbWhite
    ws.Cells(rowNum, errorCol).ClearContents
End Sub

' ============================================================
' Write error message and highlight the failed cell.
' Resolves errorCol from sheet config internally.
' Call this after ValidateRow when result.Passed = False.
' ============================================================
Public Sub OutputError(ws As Worksheet, rowNum As Long, result As ValidationResult)
    Dim sheetConfDict As Object: Set sheetConfDict = GetSheetConfig()
    Dim sheetConf As Object: Set sheetConf = sheetConfDict(ws.CodeName)
    Dim errorCol As Long: errorCol = ColNum(CStr(sheetConf("ErrorCol")))

    If result.Passed Then
        If Not StartsWith(ws.Cells(rowNum, errorCol).Value, "W") Then
            ws.Cells(rowNum, errorCol).ClearContents
        End If
    Else
        Dim cellAddr As String: cellAddr = ColLetter(result.ErrorCol)
        ws.Cells(rowNum, errorCol).Value = GetErrorMsg(result.ErrorCode, cellAddr & CStr(result.ErrorRow), result.Extra)
        ws.Cells(rowNum, result.ErrorCol).Interior.Color = RGB(255, 0, 0)
    End If
End Sub