' ========================================
' 类模块: clsBOMManager
' 用途: 管理整个BOM数据结构
' 功能:
'   1. 加载和组织BOM数据
'   2. 建立类别层级关系
'   3. 提供物料查询接口
'   4. 支持领料逻辑处理
' ========================================
Option Explicit

' ========================================
' 私有成员变量
' ========================================
Private dictCategories As Object    ' Dictionary对象: 类别名称 -> clsCategory对象
                                    ' 作用: 快速查找任意类别
Private dictAllMaterials As Object  ' Dictionary对象: 物料代号 -> clsMaterialItem对象
                                    ' 作用: 快速查找任意物料
Private rootCategories As collection ' Collection: 存储所有顶层类别(无父类别的类别)
                                    ' 作用: 遍历完整的类别树结构

' ========================================
' 类初始化
' 说明: 创建BOMManager实例时自动调用
' ========================================
Private Sub Class_Initialize()
    Set dictCategories = CreateObject("Scripting.Dictionary")
    Set dictAllMaterials = CreateObject("Scripting.Dictionary")
    Set rootCategories = New collection
End Sub

' ========================================
' LoadData 方法
' 功能: 从Excel工作表加载BOM数据并构建数据结构
' 参数:
'   wsConfig   - [领料配置]工作表对象,包含类别层级和需要领料的物料
'   wsPlatform - [平台配置清单]工作表对象,包含完整的物料信息(代号/名称/数量/条件)
' 处理步骤:
'   1. 从[平台配置清单]加载所有物料的基础信息到dictAllMaterials
'   2. 从[领料配置]加载类别信息,筛选需要领料的物料
'   3. 建立类别的父子关系,构建层级树
' 注意:
'   - 只有出现在[领料配置]中的物料才会被添加到类别中
'   - 未在[领料配置]中的物料表示不需要领料
' ========================================
Public Sub LoadData(wsConfig As Worksheet, wsPlatform As Worksheet)
    Dim i As Long, lastRow As Long
    Dim mat As clsMaterialItem
    Dim cat As clsCategory

    ' ========================================
    ' 第一步: 从平台配置清单加载所有物料的基础信息
    ' 说明:
    '   - 读取C列(代号)、D列(名称)、E列(数量)、F列(选择条件)
    '   - 从第4行开始读取(前3行是标题)
    '   - 所有物料存入dictAllMaterials字典,以代号为键
    ' 目的: 建立完整的物料信息库,供后续按代号查询
    ' ========================================
    lastRow = wsPlatform.Cells(wsPlatform.Rows.Count, "C").End(xlUp).row
    For i = 4 To lastRow ' 从第4行开始(跳过标题)
        Set mat = New clsMaterialItem
        mat.code = Trim(wsPlatform.Cells(i, "C").value & "")       ' 物料代号
        mat.Name = Trim(wsPlatform.Cells(i, "D").value & "")       ' 物料名称
        On Error Resume Next
        mat.Quantity = CDbl(wsPlatform.Cells(i, "E").value)        ' 物料数量
        On Error GoTo 0
        mat.Condition = Trim(wsPlatform.Cells(i, "F").value & "")  ' 选择条件

        ' 只保存代号非空的物料
        If mat.code <> "" Then
            Set dictAllMaterials(mat.code) = mat
        End If
    Next i

    ' ========================================
    ' 第二步: 从领料配置加载类别信息并建立层级
    ' 说明:
    '   - 读取A列(代号)、C列(类别)、D列(上层类别)
    '   - 从第2行开始读取(第1行是标题)
    '   - 只有出现在此表中的物料才需要领料
    ' 处理逻辑:
    '   1. 为每个类别创建clsCategory对象
    '   2. 从dictAllMaterials中查找对应的物料信息
    '   3. 将物料添加到对应的类别中
    ' ========================================
    lastRow = wsConfig.Cells(wsConfig.Rows.Count, "A").End(xlUp).row
    For i = 2 To lastRow ' 从第2行开始
        Dim code As String, catName As String, parentCatName As String
        code = Trim(wsConfig.Cells(i, "A").value & "")           ' 物料代号
        catName = Trim(wsConfig.Cells(i, "C").value & "")        ' 类别名称
        parentCatName = Trim(wsConfig.Cells(i, "D").value & "")  ' 上层类别名称

        ' 跳过空行
        If code = "" Then GoTo NextRow

        ' 确保类别对象存在(如果类别不存在则创建)
        If Not dictCategories.Exists(catName) Then
            Set cat = New clsCategory
            cat.categoryName = catName
            cat.ParentCategoryName = parentCatName
            Set dictCategories(catName) = cat
        End If

        ' 将物料添加到类别
        ' 注意: 必须先在dictAllMaterials中查找到完整的物料信息
        If dictAllMaterials.Exists(code) Then
            Set mat = dictAllMaterials(code)
            mat.Category = catName          ' 设置物料所属类别
            mat.ParentCategory = parentCatName  ' 设置物料的上层类别
            dictCategories(catName).AddMaterial mat  ' 将物料添加到类别对象中
        End If

NextRow:
    Next i

    ' ========================================
    ' 第三步: 建立类别层级关系
    ' 说明:
    '   - 遍历所有类别,根据ParentCategoryName建立父子关系
    '   - 如果类别有父类别,将自己添加到父类别的SubCategories中
    '   - 如果类别没有父类别,则为根类别,添加到rootCategories中
    ' 结果:
    '   - 构建完整的树形结构
    '   - rootCategories包含所有顶层类别
    '   - 每个类别的SubCategories包含其直接子类别
    ' ========================================
    Dim key As Variant
    For Each key In dictCategories.Keys
        Set cat = dictCategories(key)
        If cat.ParentCategoryName <> "" Then
            ' 有父类别,建立父子关系
            If dictCategories.Exists(cat.ParentCategoryName) Then
                Dim parentCat As clsCategory
                Set parentCat = dictCategories(cat.ParentCategoryName)
                parentCat.AddSubCategory cat  ' 将当前类别添加为父类别的子类别
            End If
        Else
            ' 无父类别,是根类别
            rootCategories.Add cat, cat.categoryName
        End If
    Next key
End Sub

' ========================================
' GetRootCategories 方法
' 功能: 获取所有顶层类别的集合
' 返回: Collection对象,包含所有无父类别的clsCategory对象
' 用途:
'   - 遍历整个BOM结构时的入口点
'   - 生成领料清单时遍历所有根类别
' 示例:
'   Dim cats As Collection
'   Set cats = bomMgr.GetRootCategories()
'   For i = 1 To cats.Count
'       Debug.Print cats(i).CategoryName
'   Next i
' ========================================
Public Function GetRootCategories() As collection
    Set GetRootCategories = rootCategories
End Function

' ========================================
' GetCategory 方法
' 功能: 根据类别名称获取类别对象
' 参数:
'   categoryName - 要查询的类别名称(字符串)
' 返回:
'   clsCategory对象 - 如果找到
'   Nothing         - 如果未找到
' 用途: 快速查找特定类别及其下的物料
' 示例:
'   Dim cat As clsCategory
'   Set cat = bomMgr.GetCategory("部件")
'   If Not cat Is Nothing Then
'       Debug.Print cat.Materials.Count & " 个物料"
'   End If
' ========================================
Public Function GetCategory(categoryName As String) As clsCategory
    If dictCategories.Exists(categoryName) Then
        Set GetCategory = dictCategories(categoryName)
    Else
        Set GetCategory = Nothing
    End If
End Function

' ========================================
' GetMaterialsForPicking 方法
' 功能: 获取某类别下需要领料的物料清单(考虑层级逻辑)
' 参数:
'   categoryName - 类别名称
'   useParent    - 可选参数,默认True
'                  True:  使用父类别物料(默认领料方式)
'                  False: 使用子类别物料(库存不足时的替代方案)
' 返回: Collection对象,包含clsMaterialItem对象
'
' 业务逻辑说明:
'   1. 默认领取父类别的物料(如"低压接头部件")
'   2. 当父类别库存不足时,才领取子类别的物料(如"接头"+"弹性元件")
'   3. 如果useParent=True但类别有子类别,仍返回父类别物料
'   4. 如果useParent=False,递归获取所有子类别的物料
'
' 示例1: 获取"部件"类别的物料(父类别)
'   Set mats = bomMgr.GetMaterialsForPicking("部件", True)
'   ' 返回: 低压接头部件、高压接头部件等组装好的部件
'
' 示例2: 获取"部件"类别的物料(子类别展开)
'   Set mats = bomMgr.GetMaterialsForPicking("部件", False)
'   ' 返回: 径向低压接头、弹簧管、螺旋管等零件
' ========================================
Public Function GetMaterialsForPicking(categoryName As String, _
                                      Optional useParent As Boolean = True) As collection
    Dim result As collection
    Set result = New collection

    ' 查找指定类别
    Dim cat As clsCategory
    Set cat = GetCategory(categoryName)
    If cat Is Nothing Then
        ' 类别不存在,返回空集合
        Set GetMaterialsForPicking = result
        Exit Function
    End If

    Dim i As Long

    If useParent Then
        ' ========================================
        ' 使用父类别物料(默认领料方式)
        ' 说明:
        '   - 直接返回当前类别下的所有物料
        '   - 即使该类别有子类别,也仍然返回父类别物料
        '   - 这是正常情况下的领料方式(领取组装好的部件)
        ' ========================================
        Dim m As clsMaterialItem
        For i = 1 To cat.materials.Count
            Set m = cat.materials(i)
            result.Add m
        Next i
    Else
        ' ========================================
        ' 使用子类别物料(库存不足时的替代方案)
        ' 说明:
        '   - 如果当前类别有子类别,递归获取所有子类别的物料
        '   - 如果当前类别是叶子类别(无子类别),返回本类别物料
        '   - 这用于父类别库存不足,需要领取零件自行组装的情况
        ' 示例:
        '   当"低压接头部件"库存不足时
        '   改为领取"径向低压接头"+"弹簧管"零件
        ' ========================================
        If cat.HasSubCategories Then
            ' 有子类别,递归获取所有子类别的物料
            Dim subCat As clsCategory
            Dim j As Long
            For j = 1 To cat.SubCategories.Count
                Set subCat = cat.SubCategories(j)
                Dim subMats As collection
                ' 递归调用,继续展开子类别
                Set subMats = GetMaterialsForPicking(subCat.categoryName, False)
                Dim k As Long
                For k = 1 To subMats.Count
                    result.Add subMats(k)
                Next k
            Next j
        Else
            ' 叶子类别,返回本类别物料
            For i = 1 To cat.materials.Count
                Set m = cat.materials(i)
                result.Add m
            Next i
        End If
    End If

    Set GetMaterialsForPicking = result
End Function

' ========================================
' PrintCategoryTree 方法
' 功能: 将类别树结构打印到工作表(用于调试和查看)
' 参数:
'   ws - 输出的目标工作表对象
' 输出格式:
'   - 第一列: 类别名称(带缩进显示层级)
'   - 第二列: 物料数量信息
'   - 第三列: 选择条件
' 说明:
'   - 使用缩进显示类别层级(每层2个空格)
'   - 递归打印所有子类别和物料
'   - 便于验证数据结构是否正确
' 示例输出:
'   表壳 (物料数:1)
'     - 01091004312 表壳(本色)  数量:1  条件:
'   部件 (物料数:20)
'     - 01011019001 低压接头部件  数量:1  条件:lcfw=M02...
'     接头 (物料数:18)
'       - 01081013833 径向低压接头  数量:1  条件:gclj=Z12...
' ========================================
Public Sub PrintCategoryTree(ws As Worksheet)
    Dim row As Long
    row = 1
    ws.Cells(row, 1).value = "类别层级结构"
    row = row + 1

    ' 遍历所有根类别,递归打印整个树
    Dim rootCat As clsCategory
    Dim i As Long
    For i = 1 To rootCategories.Count
        Set rootCat = rootCategories(i)
        Call PrintCategory(ws, rootCat, row, 0)
    Next i
End Sub

' ========================================
' PrintCategory 方法 (私有方法)
' 功能: 递归打印单个类别及其子类别(供PrintCategoryTree调用)
' 参数:
'   ws    - 输出的工作表对象
'   cat   - 要打印的类别对象
'   row   - 当前输出行号(ByRef,会被修改)
'   level - 当前层级深度(0=根类别,1=一级子类别...)
' 说明:
'   - 使用递归方式遍历整个类别树
'   - 根据level参数计算缩进空格数
'   - 先打印类别名,再打印该类别的所有物料,最后递归打印子类别
' ========================================
Private Sub PrintCategory(ws As Worksheet, cat As clsCategory, _
                         ByRef row As Long, level As Integer)
    ' 计算缩进(每层2个空格)
    Dim indent As String
    indent = String(level * 2, " ")

    ' 打印类别名称和物料数量统计
    ws.Cells(row, 1).value = indent & cat.categoryName & _
                            " (物料数:" & cat.materials.Count & ")"
    row = row + 1

    ' 打印该类别下的所有物料
    Dim mat As clsMaterialItem
    Dim i As Long
    For i = 1 To cat.materials.Count
        Set mat = cat.materials(i)
        ' 物料行额外缩进2个空格,并加上"- "前缀
        ws.Cells(row, 1).value = indent & "  - " & mat.code & " " & mat.Name
        ws.Cells(row, 2).value = "数量:" & mat.Quantity
        ws.Cells(row, 3).value = "条件:" & mat.Condition
        row = row + 1
    Next i

    ' 递归打印所有子类别
    Dim subCat As clsCategory
    Dim j As Long
    For j = 1 To cat.SubCategories.Count
        Set subCat = cat.SubCategories(j)
        ' 递归调用,层级加1
        Call PrintCategory(ws, subCat, row, level + 1)
    Next j
End Sub
' ========================================
' GetMaterialsByModel 方法
'
' 【功能概述】
'   根据产品型号字符串自动解析规格参数,并返回符合这些规格的所有物料清单。
'   这是BOM系统的核心方法,实现了从"产品型号"到"物料清单"的智能转换。
'
' 【工作流程】
'   1. 型号解析 (clsModelParser) → 将产品型号字符串拆解为结构化参数
'   2. 条件提取 (clsConditionExtractor) → 从型号参数中提取匹配条件变量
'   3. 物料筛选 (clsConditionMatcher) → 根据条件从物料库中筛选符合条件的物料
'
' 【参数说明】
'   modelStr - 产品型号字符串
'              格式: [型号]-[口径].[安装].[表壳].[连接].[量程]|[表盘]|[附件]|[法兰]
'              示例: "YTHN-100.A0.532.M203.M16.Y3|BP-095.2312.M16.PA3"
'              说明: 必须是完整的型号字符串,不能为空
'
'   categoryName - 类别名称(可选参数,默认为空字符串)
'              为空字符串 "": 返回所有类别的符合条件物料(默认行为)
'              指定类别名:  仅返回该类别下的符合条件物料
'              示例: "部件"、"表壳"、"接头"、"机芯"
'
' 【返回值】
'   返回类型: Collection对象
'   元素类型: Collection中的每个元素都是 clsMaterialItem 对象
'
'   clsMaterialItem 对象属性:
'     .code      - String  - 物料代号(如: "01011019001")
'     .Name      - String  - 物料名称(如: "低压接头部件")
'     .Quantity  - Double  - 物料数量(如: 1, 2, 0.5)
'     .Condition - String  - 选择条件(如: "lcfw=M02 AND gclj=Z12")
'     .Category  - String  - 所属类别名称
'     .ParentCategory - String - 上层类别名称
'
' 【注意事项】
'   1. 调用前必须先执行 LoadData() 方法加载BOM数据
'   2. 型号字符串必须完整且符合格式要求
'   3. 返回的Collection可能为空(没有符合条件的物料),需要判断Count属性
'   4. 调试信息会输出到VBA的"立即窗口"(Ctrl+G查看)
'   5. 物料的Condition属性为空表示该物料无条件限制(所有型号都使用)
'
' 【相关方法】
'   - GetMaterialsByCategoryAndModel: 结合了类别层级逻辑的物料获取
'   - GetMaterialsForPicking: 纯粹按类别获取物料(不进行型号匹配)
'   - ParseModelAndExtractConditions: 仅解析型号并返回条件字典
' ========================================
Public Function GetMaterialsByModel(modelStr As String, _
                                   Optional categoryName As String = "") As collection
    Dim result As collection
    Set result = New collection

    ' 1. 解析型号
    Dim parser As New clsModelParser
    If Not parser.ParseModel(modelStr) Then
        Debug.Print "型号解析失败: " & parser.ErrorMessage
        Set GetMaterialsByModel = result
        Exit Function
    End If

    ' 2. 提取条件
    Dim extractor As New clsConditionExtractor
    Dim conditions As Object
    Set conditions = extractor.ExtractConditions(parser)

    ' 调试输出
    Debug.Print "【型号】: " & modelStr
    Debug.Print "【提取的条件】:"
    Dim key As Variant
    For Each key In conditions.Keys
        Debug.Print "  " & key & " = " & conditions(key)
    Next key
    Debug.Print ""

    ' 3. 创建条件匹配器
    Dim matcher As New clsConditionMatcher

    ' 4. 获取要筛选的物料范围
    Dim materialsToFilter As collection
    Set materialsToFilter = New collection

    If categoryName <> "" Then
        ' 指定类别：获取该类别的所有物料
        Dim cat As clsCategory
        Set cat = GetCategory(categoryName)
        If Not cat Is Nothing Then
            Dim i As Long
            For i = 1 To cat.materials.Count
                materialsToFilter.Add cat.materials(i)
            Next i
        End If
    Else
        ' 未指定类别：获取所有根类别的物料
        Dim rootCat As clsCategory
        Dim j As Long
        For j = 1 To rootCategories.Count
            Set rootCat = rootCategories(j)
            ' 递归获取所有物料
            Call CollectAllMaterials(rootCat, materialsToFilter)
        Next j
    End If

    ' 5. 筛选符合条件的物料
    Dim mat As clsMaterialItem
    Dim matchCount As Long
    matchCount = 0

    For Each mat In materialsToFilter
        ' 使用条件匹配器判断
        If matcher.IsMatch(mat.Condition, conditions) Then
            result.Add mat
            matchCount = matchCount + 1

            ' 调试输出
            Debug.Print "【匹配】 " & mat.code & " - " & mat.Name & _
                       " | 条件: " & IIf(mat.Condition = "", "(无)", mat.Condition)
        End If
    Next mat

    Debug.Print "共匹配 " & matchCount & " 个物料"
    Debug.Print String(60, "=")

    Set GetMaterialsByModel = result
End Function

' ========================================
' CollectAllMaterials 方法 (私有)
' 功能: 递归收集类别及其所有子类别的物料
' 参数:
'   cat - 类别对象
'   collection - 用于存储物料的Collection
' ========================================
Private Sub CollectAllMaterials(cat As clsCategory, collection As collection)
    Dim i As Long

    ' 添加当前类别的所有物料
    For i = 1 To cat.materials.Count
        collection.Add cat.materials(i)
    Next i

    ' 递归处理子类别
    Dim subCat As clsCategory
    Dim j As Long
    For j = 1 To cat.SubCategories.Count
        Set subCat = cat.SubCategories(j)
        Call CollectAllMaterials(subCat, collection)
    Next j
End Sub

' ========================================
' GetMaterialsByCategoryAndModel 方法
' 功能: 根据类别和型号获取物料（使用父类别或子类别逻辑）
' 参数:
'   modelStr - 产品型号字符串
'   categoryName - 类别名称
'   useParent - True=使用父类别物料，False=使用子类别物料
' 返回: Collection对象，包含符合条件的clsMaterialItem对象
' 说明: 这是 GetMaterialsForPicking 和 GetMaterialsByModel 的结合
' ========================================
Public Function GetMaterialsByCategoryAndModel(modelStr As String, _
                                              categoryName As String, _
                                              Optional useParent As Boolean = True) As collection
    Dim result As collection
    Set result = New collection

    ' 1. 解析型号并提取条件
    Dim parser As New clsModelParser
    If Not parser.ParseModel(modelStr) Then
        Set GetMaterialsByCategoryAndModel = result
        Exit Function
    End If

    Dim extractor As New clsConditionExtractor
    Dim conditions As Object
    Set conditions = extractor.ExtractConditions(parser)

    ' 2. 获取类别的物料（根据 useParent 参数）
    Dim materialsToFilter As collection
    Set materialsToFilter = GetMaterialsForPicking(categoryName, useParent)

    ' 3. 筛选符合条件的物料
    Dim matcher As New clsConditionMatcher
    Dim mat As clsMaterialItem

    For Each mat In materialsToFilter
        If matcher.IsMatch(mat.Condition, conditions) Then
            result.Add mat
        End If
    Next mat

    Set GetMaterialsByCategoryAndModel = result
End Function

' ========================================
' ParseModelAndExtractConditions 方法
' 功能: 解析型号并返回条件字典（工具方法）
' 参数: modelStr - 产品型号字符串
' 返回: Dictionary对象，包含提取的条件
' 用途: 供外部调用，用于查看提取的条件
' ========================================
Public Function ParseModelAndExtractConditions(modelStr As String) As Object
    Dim parser As New clsModelParser
    Dim extractor As New clsConditionExtractor
    Dim conditions As Object

    If parser.ParseModel(modelStr) Then
        Set conditions = extractor.ExtractConditions(parser)
    Else
        Set conditions = CreateObject("Scripting.Dictionary")
    End If

    Set ParseModelAndExtractConditions = conditions
End Function