Option Explicit

' ============================
' CSV エクスポート（UTF-8 BOM付き）
' H6セルのシート名を対象に出力
' 1行目に [code:label] メタがある列は code で出力
' ============================

' === 設定 ===
Private Const EXPORT_UTF8_WITH_BOM As Boolean = True
Private Const STRIP_META_FROM_HEADER As Boolean = False ' Trueにするとヘッダから [x:ラベル] を削除して出力

' 入口：ボタンに割り当て
Public Sub Run_ExportCsv_FromH6()
    Dim ctrl As Worksheet
    Set ctrl = ActiveSheet

    Dim targetName As String
    targetName = Trim$(Nz(ctrl.Range("H6").value, ""))
    If Len(targetName) = 0 Then
        MsgBox "H6セルに出力対象のシート名がありません。", vbExclamation
        Exit Sub
    End If
    If Not SheetExists(targetName) Then
        MsgBox "シート """ & targetName & """ が見つかりません。", vbExclamation
        Exit Sub
    End If

    Dim ws As Worksheet
    Set ws = ThisWorkbook.Worksheets(targetName)

    ExportCsvWithMeta ws
End Sub

' === 本体 ===
Private Sub ExportCsvWithMeta(ByVal ws As Worksheet)
    Dim lastRow As Long, lastCol As Long
    lastRow = GetLastRow(ws)
    lastCol = GetLastCol(ws)
    If lastRow = 0 Or lastCol = 0 Then
        MsgBox "対象シートが空です。", vbExclamation
        Exit Sub
    End If

    ' ヘッダ行（1行目）を読み、列ごとにメタ（[code:label]）を辞書化
    Dim colHasEnum() As Boolean
    Dim mapLabelToCode() As Object ' Scripting.Dictionary（label→code）
    Dim setValidCode() As Object    ' Scripting.Dictionary（code→True）
    ReDim colHasEnum(1 To lastCol)
    ReDim mapLabelToCode(1 To lastCol)
    ReDim setValidCode(1 To lastCol)

    Dim c As Long
    For c = 1 To lastCol
        Dim headerRaw As String
        headerRaw = Nz(ws.Cells(1, c).value, "")
        BuildEnumMaps headerRaw, mapLabelToCode(c), setValidCode(c), colHasEnum(c)
    Next c

    ' 一括読み
    Dim rng As Range
    Set rng = ws.Range("A1").Resize(lastRow, lastCol)
    Dim data As Variant
    data = rng.value ' 1-based

    ' CSV行を構築
    Dim sb As String
    Dim r As Long

    ' 1行目（ヘッダ）
    Dim line As String
    line = ""
    For c = 1 To lastCol
        Dim h As String
        h = Nz(data(1, c), "")
        If STRIP_META_FROM_HEADER Then
            h = StripEnumFromHeader(h)
        End If
        AppendCsvField line, h
    Next c
    sb = line

    ' データ行
    For r = 2 To lastRow
        ' 行全体が空ならスキップ（全列空 or 空白のみ）
        If RowIsAllEmpty(data, r, lastCol) Then
            ' スキップ
        Else
            line = ""
            For c = 1 To lastCol
                Dim v As String
                v = Nz(data(r, c), "")

                If colHasEnum(c) Then
                    Dim codeOut As String, ok As Boolean
                    ok = TryResolveToCode(v, mapLabelToCode(c), setValidCode(c), codeOut)
                    If Not ok Then
                        ' エラー表示＆該当セルを選択して中断
                        Dim addr As String
                        addr = ws.Cells(r, c).Address(False, False)
                        MsgBox "列挙メタに一致しない値が見つかりました。" & vbCrLf & _
                               "セル: " & addr & vbCrLf & _
                               "値  : " & v & vbCrLf & _
                               "ヘッダ: " & Nz(data(1, c), ""), vbCritical
                        ws.Activate
                        ws.Cells(r, c).Select
                        Exit Sub
                    End If
                    AppendCsvField line, codeOut
                Else
                    AppendCsvField line, v
                End If
            Next c
            sb = sb & vbCrLf & line
        End If
    Next r

    ' 保存ダイアログ
    Dim fpath As String
    fpath = PickSavePathCsv(ws.Name)
    If Len(fpath) = 0 Then
        MsgBox "保存がキャンセルされました。", vbInformation
        Exit Sub
    End If

    ' UTF-8で書き込み
    If EXPORT_UTF8_WITH_BOM Then
        WriteUtf8WithBom fpath, sb
    Else
        WriteUtf8NoBom fpath, sb
    End If

    MsgBox "CSVの書き出しが完了しました。" & vbCrLf & fpath, vbInformation
End Sub

' === メタ解析（[code:label] または [code：label] を辞書化） ===
Private Sub BuildEnumMaps(ByVal headerRaw As String, _
                          ByRef dictLabelToCode As Object, _
                          ByRef dictValidCode As Object, _
                          ByRef hasEnum As Boolean)
    Dim re As Object, mc As Object, m As Object
    Set re = CreateObject("VBScript.RegExp")
    re.Pattern = "\[(\d+)[:：]([^\]]+)\]"
    re.IgnoreCase = True
    re.Global = True

    hasEnum = False
    Set dictLabelToCode = CreateObject("Scripting.Dictionary")
    Set dictValidCode = CreateObject("Scripting.Dictionary")

    If re.Test(headerRaw) Then
        Set mc = re.Execute(headerRaw)
        Dim code As String, label As String, key As String
        For Each m In mc
            code = CStr(m.SubMatches(0))
            label = CStr(m.SubMatches(1))
            ' 有効コードセット
            If Not dictValidCode.Exists(code) Then dictValidCode.Add code, True
            ' ラベル→コード（トリム＆半角化キーも登録）
            key = NormalizeKey(label)
            If Not dictLabelToCode.Exists(key) Then dictLabelToCode.Add key, code

            ' ラベルのバリエーション（両端空白除去のみ）
            key = NormalizeKeyBasic(label)
            If Not dictLabelToCode.Exists(key) Then dictLabelToCode.Add key, code

            hasEnum = True
        Next m
    End If
End Sub

' === 値 → code への正規化 ===
' 空は許容（空文字を返す）。一致しなければ False
Private Function TryResolveToCode(ByVal v As String, _
                                  ByVal dictLabelToCode As Object, _
                                  ByVal dictValidCode As Object, _
                                  ByRef outCode As String) As Boolean
    Dim s As String
    s = TrimBoth(v)
    If Len(s) = 0 Then
        outCode = ""
        TryResolveToCode = True
        Exit Function
    End If

    ' 1) "code:label" / "code：label" 形式
    Dim re As Object, m As Object
    Set re = CreateObject("VBScript.RegExp")
    re.Pattern = "^\s*(\d+)\s*[:：]\s*.+$"
    re.IgnoreCase = True
    re.Global = False
    If re.Test(s) Then
        Set m = re.Execute(s)(0)
        If dictValidCode.Exists(m.SubMatches(0)) Then
            outCode = CStr(m.SubMatches(0))
            TryResolveToCode = True
            Exit Function
        Else
            TryResolveToCode = False
            Exit Function
        End If
    End If

    ' 2) code そのもの
    ' If dictValidCode.Exists(s) Then
    '     outCode = s
    '     TryResolveToCode = True
    '     Exit Function
    ' End If

    ' 2b) 全角→半角にして code 判定
    ' Dim sN As String
    ' On Error Resume Next
    ' sN = StrConv(s, vbNarrow)
    ' On Error GoTo 0
    ' If Len(sN) > 0 Then
    '     If dictValidCode.Exists(sN) Then
    '         outCode = sN
    '         TryResolveToCode = True
    '         Exit Function
    '     End If
    ' End If

    ' 3) label として一致（正規化キーで判定）
    Dim key As String
    key = NormalizeKey(s)
    If dictLabelToCode.Exists(key) Then
        outCode = CStr(dictLabelToCode(key))
        TryResolveToCode = True
        Exit Function
    End If

    ' 3b) ラベル・最小正規化
    key = NormalizeKeyBasic(s)
    If dictLabelToCode.Exists(key) Then
        outCode = CStr(dictLabelToCode(key))
        TryResolveToCode = True
        Exit Function
    End If

    TryResolveToCode = False
End Function

' === ヘッダから [x:ラベル] を削除 ===
Private Function StripEnumFromHeader(ByVal headerRaw As String) As String
    Dim re As Object
    Set re = CreateObject("VBScript.RegExp")
    re.Pattern = "\[[^\]]+\]"
    re.Global = True
    StripEnumFromHeader = Trim$(re.Replace(headerRaw, ""))
End Function

' === CSVフィールド追加（RFC準拠の引用） ===
Private Sub AppendCsvField(ByRef line As String, ByVal value As String)
    Dim s As String
    s = CStr(value)
    ' 改行はCRLFに正規化
    s = Replace(s, vbCrLf, vbLf)
    s = Replace(s, vbCr, vbLf)
    s = Replace(s, vbLf, vbCrLf)

    ' ダブルクォートは二重化
    If InStr(1, s, """") > 0 Then s = Replace(s, """", """""""")
    ' 区切り・改行・引用符を含む場合は引用
    If (InStr(1, s, ",") > 0) Or (InStr(1, s, vbCrLf) > 0) Or (InStr(1, s, """") > 0) Then
        s = """" & s & """"
    End If

    If Len(line) = 0 Then
        line = s
    Else
        line = line & "," & s
    End If
End Sub

' === 保存ダイアログ ===
Private Function PickSavePathCsv(ByVal baseName As String) As String
    Dim fd As FileDialog
    Set fd = Application.FileDialog(msoFileDialogSaveAs)
    With fd
        .Title = "CSVファイルの保存先を選択"
        .FilterIndex = 1
        .InitialFileName = baseName & "_" & Format(Now, "yymmdd_hhnnss") & ".csv"
        If .Show = -1 Then
            PickSavePathCsv = .SelectedItems(1)
            ' 拡張子補完
            If LCase$(Right$(PickSavePathCsv, 4)) <> ".csv" Then
                PickSavePathCsv = PickSavePathCsv & ".csv"
            End If
        Else
            PickSavePathCsv = ""
        End If
    End With
End Function

' === UTF-8（BOM付き）で書き出し ===
Private Sub WriteUtf8WithBom(ByVal fpath As String, ByVal content As String)
    Dim stmText As Object, stmBin As Object
    Dim bom(2) As Byte
    bom(0) = &HEF: bom(1) = &HBB: bom(2) = &HBF

    ' テキスト→UTF-8 バイトへ
    Set stmText = CreateObject("ADODB.Stream")
    stmText.Type = 2 ' adTypeText
    stmText.Charset = "utf-8"
    stmText.Open
    stmText.WriteText content
    stmText.Position = 0

    ' BOM + 本文をバイナリで保存
    Set stmBin = CreateObject("ADODB.Stream")
    stmBin.Type = 1 ' adTypeBinary
    stmBin.Open
    stmBin.Write bom
    stmText.CopyTo stmBin
    stmBin.SaveToFile fpath, 2 ' adSaveCreateOverWrite

    stmText.Close: stmBin.Close
End Sub

' === UTF-8（BOMなし）で書き出し ===
Private Sub WriteUtf8NoBom(ByVal fpath As String, ByVal content As String)
    Dim stm As Object
    Set stm = CreateObject("ADODB.Stream")
    stm.Type = 2 ' adTypeText
    stm.Charset = "utf-8"
    stm.Open
    stm.WriteText content
    stm.SaveToFile fpath, 2 ' adSaveCreateOverWrite
    stm.Close
End Sub

' === 行が全空か ===
Private Function RowIsAllEmpty(ByRef data As Variant, ByVal r As Long, ByVal lastCol As Long) As Boolean
    Dim c As Long, s As String
    For c = 1 To lastCol
        s = TrimBoth(Nz(data(r, c), ""))
        If Len(s) > 0 Then
            RowIsAllEmpty = False
            Exit Function
        End If
    Next c
    RowIsAllEmpty = True
End Function

' === 便利関数群 ===
Private Function GetLastRow(ByVal ws As Worksheet) As Long
    Dim f As Range
    On Error Resume Next
    Set f = ws.Cells.Find(What:="*", After:=ws.Range("A1"), LookAt:=xlPart, _
                          LookIn:=xlFormulas, SearchOrder:=xlByRows, _
                          SearchDirection:=xlPrevious, MatchCase:=False)
    On Error GoTo 0
    If f Is Nothing Then GetLastRow = 0 Else GetLastRow = f.Row
End Function

Private Function GetLastCol(ByVal ws As Worksheet) As Long
    Dim f As Range
    On Error Resume Next
    Set f = ws.Cells.Find(What:="*", After:=ws.Range("A1"), LookAt:=xlPart, _
                          LookIn:=xlFormulas, SearchOrder:=xlByColumns, _
                          SearchDirection:=xlPrevious, MatchCase:=False)
    On Error GoTo 0
    If f Is Nothing Then GetLastCol = 0 Else GetLastCol = f.Column
End Function

Private Function SheetExists(ByVal sName As String) As Boolean
    Dim ws As Worksheet
    On Error Resume Next
    Set ws = ThisWorkbook.Worksheets(sName)
    SheetExists = Not ws Is Nothing
    On Error GoTo 0
End Function

Private Function Nz(ByVal v As Variant, Optional ByVal defaultValue As String = "") As String
    If IsError(v) Then
        Nz = defaultValue
    ElseIf IsNull(v) Or IsEmpty(v) Then
        Nz = defaultValue
    Else
        Nz = CStr(v)
    End If
End Function

' 半角・全角スペースを両方トリム
Private Function TrimBoth(ByVal s As String) As String
    Dim t As String
    t = s
    t = Replace(t, ChrW(&H3000), " ") ' 全角スペース→半角スペース
    TrimBoth = Trim$(t)
End Function

' ラベル用 正規化キー（両端空白除去 + 全角→半角 + 連続空白縮約）
Private Function NormalizeKey(ByVal s As String) As String
    Dim t As String
    t = TrimBoth(s)
    On Error Resume Next
    t = StrConv(t, vbNarrow)
    On Error GoTo 0
    ' 空白の縮約
    Do While InStr(t, "  ") > 0
        t = Replace(t, "  ", " ")
    Loop
    NormalizeKey = t
End Function

' ラベル用 簡易キー（両端トリムのみ）
Private Function NormalizeKeyBasic(ByVal s As String) As String
    NormalizeKeyBasic = TrimBoth(s)
End Function

