'@Folder modRepositoryImport
Option Explicit

' ==========================================================
'  Excel VBA 프로젝트 가져오기 도구
'  대상: 활성 통합 문서의 VBProject
'
'  가져오기:
'   - BAS → 표준 모듈 교체
'   - CLS → 클래스 모듈 교체
'   - CSV → UserForm 레이아웃 반영
'   - CLS → UserForm 코드 반영
'   - CLS → ThisWorkbook / Sheet 모듈 반영
' ==========================================================

Private Const vbext_ct_StdModule As Long = 1
Private Const vbext_ct_ClassModule As Long = 2
Private Const vbext_ct_MSForm As Long = 3
Private Const vbext_ct_Document As Long = 100

Private Const vbext_pp_locked As Long = 1

Private Const MAX_PROBLEMS_IN_MESSAGE As Long = 15

' 가져오기 중 발생한 오류·건너뜀 목록
Private m_problems As Collection

Public Sub ImportRepositoryForActiveWorkbook()

    Dim wb As Workbook
    Dim vbproj As Object
    Dim vbComp As Object

    Dim baseFolder As String
    Dim formsCsvFolder As String
    Dim formCodeFolder As String
    Dim modulesFolder As String
    Dim classesFolder As String
    Dim documentsFolder As String

    Set wb = ActiveWorkbook

    If wb Is Nothing Then
        MsgBox "활성화된 통합 문서가 없습니다.", vbExclamation
        Exit Sub
    End If

    If wb Is ThisWorkbook Then
        MsgBox "Restorer가 들어 있는 통합 문서 자신에는 가져올 수 없습니다." & vbCrLf & _
               "대상 통합 문서를 활성화한 뒤 다시 실행해 주세요.", vbExclamation
        Exit Sub
    End If

    If wb.Path = vbNullString Then
        MsgBox "통합 문서가 아직 저장되지 않았습니다. 먼저 저장해 주세요.", vbExclamation
        Exit Sub
    End If

    Set vbproj = GetVBProjectSafe(wb)
    If vbproj Is Nothing Then Exit Sub

    Dim repositoryRoot As String
    Dim vbaFolder As String
    Dim formsFolder As String

    repositoryRoot = wb.Path & "\ExcelRepository"

    baseFolder = repositoryRoot & "\" & _
                 GetNameWithoutExtension(wb.Name) & "_repository"

    vbaFolder = baseFolder & "\VBA"

    formsFolder = vbaFolder & "\Forms"

    formsCsvFolder = formsFolder & "\ControlsCsv"
    formCodeFolder = formsFolder & "\Code"

    modulesFolder = vbaFolder & "\Modules"
    classesFolder = vbaFolder & "\Classes"
    documentsFolder = vbaFolder & "\Documents"

    Set m_problems = New Collection

    ImportCodeModulesFromFolder vbproj, modulesFolder, "bas", vbext_ct_StdModule
    ImportCodeModulesFromFolder vbproj, classesFolder, "cls", vbext_ct_ClassModule

    For Each vbComp In vbproj.VBComponents
        Select Case vbComp.Type
            Case vbext_ct_MSForm
                ImportFormLayoutFromCsv vbComp, formsCsvFolder
                ImportFormCodeFromCls vbComp, formCodeFolder

            Case vbext_ct_Document
                ImportDocumentModuleFromCls vbComp, documentsFolder
        End Select
    Next vbComp

    ShowImportResult baseFolder

End Sub

' ==========================================================
'  표준 모듈 / 클래스 모듈
' ==========================================================
Private Sub ImportCodeModulesFromFolder(ByVal vbproj As Object, ByVal folderPath As String, _
                                        ByVal extension As String, ByVal componentType As Long)

    Dim fso As Object
    Dim file As Object

    Set fso = CreateObject("Scripting.FileSystemObject")
    If Not fso.FolderExists(folderPath) Then Exit Sub

    For Each file In fso.GetFolder(folderPath).Files
        If LCase$(fso.GetExtensionName(file.Name)) = extension Then
            ImportCodeModuleFile vbproj, file.Path, fso.GetBaseName(file.Name), componentType
        End If
    Next file

End Sub

' ----------------------------------------------------------
'  모듈 파일 1개를 교체한다
'
'  1. 기존 모듈의 이름을 임시 이름으로 바꿔 이름을 비워 둔다
'     (Remove는 매크로가 끝날 때까지 미뤄질 수 있어서
'      바로 같은 이름으로 만들면 이름 충돌이 난다)
'  2. VBComponents.Import로 새 모듈을 만든다
'     (Attribute 줄도 함께 반영된다)
'  3. 성공하면 기존 모듈을 삭제하고,
'     실패하면 기존 모듈의 이름을 되돌린다
' ----------------------------------------------------------
Private Sub ImportCodeModuleFile(ByVal vbproj As Object, ByVal filePath As String, _
                                 ByVal moduleName As String, ByVal componentType As Long)

    Dim existing As Object
    Dim imported As Object
    Dim tempPath As String

    On Error GoTo Err_Handler

    Set existing = FindComponent(vbproj, moduleName)

    If Not existing Is Nothing Then
        If existing.Type <> componentType Then
            AddProblem moduleName, "같은 이름의 다른 종류 구성 요소(시트·폼 등)가 있어 건너뜀"
            Exit Sub
        End If
    End If

    tempPath = BuildImportTempFile(filePath, moduleName, componentType)

    If Not existing Is Nothing Then
        existing.Name = GetUnusedComponentName(vbproj, "zzOld_")
    End If

    Set imported = vbproj.VBComponents.Import(tempPath)

    If imported.Name <> moduleName Then
        imported.Name = moduleName
    End If

    If Not existing Is Nothing Then
        vbproj.VBComponents.Remove existing
    End If

CleanUp:
    If tempPath <> vbNullString Then DeleteFileQuietly tempPath
    Exit Sub

Err_Handler:
    AddProblem moduleName, Err.Number & ": " & Err.Description
    Resume Rollback

Rollback:
    On Error Resume Next
    If Not existing Is Nothing Then
        If imported Is Nothing Then existing.Name = moduleName
    End If
    On Error GoTo 0
    GoTo CleanUp

End Sub

' ----------------------------------------------------------
'  리포지토리의 UTF-8 파일을 VBE가 가져올 수 있는
'  ANSI 파일(헤더 포함)로 만들어 임시 폴더에 저장한다
' ----------------------------------------------------------
Private Function BuildImportTempFile(ByVal sourcePath As String, ByVal moduleName As String, _
                                     ByVal componentType As Long) As String

    Dim moduleAttributes As Object
    Dim header As String
    Dim body As String
    Dim key As Variant
    Dim tempPath As String

    Set moduleAttributes = CreateObject("Scripting.Dictionary")

    body = SplitModuleAttributes(ReadUtf8Text(sourcePath), moduleAttributes)

    If componentType = vbext_ct_ClassModule Then
        header = "VERSION 1.0 CLASS" & vbCrLf & _
                 "BEGIN" & vbCrLf & _
                 "  MultiUse = -1  'True" & vbCrLf & _
                 "END" & vbCrLf

        ' 파일에 적혀 있지 않은 클래스 속성은 기본값(False)
        If Not moduleAttributes.Exists("VB_GlobalNameSpace") Then moduleAttributes("VB_GlobalNameSpace") = "Attribute VB_GlobalNameSpace = False"
        If Not moduleAttributes.Exists("VB_Creatable") Then moduleAttributes("VB_Creatable") = "Attribute VB_Creatable = False"
        If Not moduleAttributes.Exists("VB_PredeclaredId") Then moduleAttributes("VB_PredeclaredId") = "Attribute VB_PredeclaredId = False"
        If Not moduleAttributes.Exists("VB_Exposed") Then moduleAttributes("VB_Exposed") = "Attribute VB_Exposed = False"

        tempPath = Environ$("TEMP") & "\VBAFerry_" & moduleName & ".cls"
    Else
        tempPath = Environ$("TEMP") & "\VBAFerry_" & moduleName & ".bas"
    End If

    header = header & "Attribute VB_Name = """ & moduleName & """" & vbCrLf

    For Each key In moduleAttributes.Keys
        header = header & moduleAttributes(key) & vbCrLf
    Next key

    Call SaveAnsiText(tempPath, header & body)

    BuildImportTempFile = tempPath

End Function

' ----------------------------------------------------------
'  코드 본문과 모듈 단위 Attribute 줄을 나눈다
'
'  - VERSION 줄, BEGIN ~ END 블록, VB_Name 은 버린다
'  - 모듈 단위 Attribute(VB_PredeclaredId 등)는
'    moduleAttributes 에 담는다
'  - 프로시저 단위 Attribute(Item.VB_UserMemId 등)는
'    본문에 그대로 남긴다
' ----------------------------------------------------------
Private Function SplitModuleAttributes(ByVal codeText As String, ByVal moduleAttributes As Object) As String

    Dim lines() As String
    Dim result As String
    Dim i As Long
    Dim oneLine As String
    Dim trimmedLine As String
    Dim attributeName As String
    Dim inBeginBlock As Boolean

    codeText = Replace(codeText, vbCrLf, vbLf)
    codeText = Replace(codeText, vbCr, vbLf)

    lines = Split(codeText, vbLf)

    For i = LBound(lines) To UBound(lines)
        oneLine = lines(i)
        trimmedLine = Trim$(oneLine)

        If Left$(trimmedLine, 8) = "VERSION " Then
            ' skip
        ElseIf trimmedLine = "BEGIN" Then
            inBeginBlock = True
        ElseIf inBeginBlock And trimmedLine = "END" Then
            inBeginBlock = False
        ElseIf inBeginBlock Then
            ' skip
        ElseIf Left$(trimmedLine, 10) = "Attribute " Then
            attributeName = GetAttributeName(trimmedLine)

            If InStr(attributeName, ".") > 0 Then
                result = result & oneLine & vbCrLf
            ElseIf attributeName <> "VB_Name" And attributeName <> vbNullString Then
                moduleAttributes(attributeName) = trimmedLine
            End If
        Else
            result = result & oneLine & vbCrLf
        End If
    Next i

    SplitModuleAttributes = result

End Function

' "Attribute VB_PredeclaredId = True" → "VB_PredeclaredId"
Private Function GetAttributeName(ByVal trimmedLine As String) As String

    Dim rest As String
    Dim pos As Long

    rest = Mid$(trimmedLine, 11)
    pos = InStr(rest, "=")

    If pos > 0 Then
        GetAttributeName = Trim$(Left$(rest, pos - 1))
    End If

End Function

' ==========================================================
'  UserForm 레이아웃
' ==========================================================
Private Sub ImportFormLayoutFromCsv(ByVal vbComp As Object, ByVal folderPath As String)

    Dim designer As Object
    Dim ctrl As Object
    Dim filePath As String
    Dim allText As String
    Dim records As Collection
    Dim parts() As String

    Dim i As Long
    Dim ctrlName As String
    Dim ctrlType As String
    Dim progId As String

    On Error GoTo Err_Handler

    Set designer = GetDesignerSafe(vbComp)
    If designer Is Nothing Then Exit Sub

    filePath = folderPath & "\" & vbComp.Name & ".csv"
    If Dir$(filePath) = vbNullString Then Exit Sub

    allText = ReadUtf8Text(filePath)
    If Len(allText) = 0 Then Exit Sub

    ' 따옴표 안의 줄바꿈(여러 줄 Caption)을 지키기 위해
    ' 줄 단위로 먼저 자르지 않고 파일 전체를 한 번에 해석한다
    Set records = ParseCsvRecords(allText)

    ' 1행은 머리글
    For i = 2 To records.Count
        parts = records(i)

        If UBound(parts) >= 6 Then
            ctrlName = parts(0)
            ctrlType = parts(1)

            If Not IsLayoutNumeric(parts) Then
                AddProblem vbComp.Name & "." & ctrlName, "Left/Top/Width/Height 가 숫자가 아니어서 건너뜀"
            Else
                Set ctrl = GetDesignerControlSafe(designer, ctrlName)

                If ctrl Is Nothing Then
                    progId = GetProgIdFromCtrlType(ctrlType)
                    If progId <> vbNullString Then
                        Set ctrl = AddDesignerControlSafe(designer, progId, ctrlName)
                    End If
                End If

                If Not ctrl Is Nothing Then
                    If ctrlType = vbNullString Or TypeName(ctrl) = ctrlType Then
                        Call ApplyControlLayout(ctrl, parts)
                    Else
                        AddProblem vbComp.Name & "." & ctrlName, _
                                   "ControlType 이 달라서 건너뜀 (Excel: " & TypeName(ctrl) & ", CSV: " & ctrlType & ")"
                    End If
                End If
            End If
        End If
    Next i

    Exit Sub

Err_Handler:
    AddProblem "UserForm 레이아웃 " & vbComp.Name, Err.Number & ": " & Err.Description

End Sub

Private Function IsLayoutNumeric(ByRef parts() As String) As Boolean

    IsLayoutNumeric = IsNumeric(parts(3)) And IsNumeric(parts(4)) And _
                      IsNumeric(parts(5)) And IsNumeric(parts(6))

End Function

Private Function GetProgIdFromCtrlType(ByVal ctrlType As String) As String

    Select Case LCase$(ctrlType)
        Case "textbox"
            GetProgIdFromCtrlType = "Forms.TextBox.1"
        Case "label"
            GetProgIdFromCtrlType = "Forms.Label.1"
        Case "commandbutton"
            GetProgIdFromCtrlType = "Forms.CommandButton.1"
        Case "combobox"
            GetProgIdFromCtrlType = "Forms.ComboBox.1"
        Case "listbox"
            GetProgIdFromCtrlType = "Forms.ListBox.1"
        Case "checkbox"
            GetProgIdFromCtrlType = "Forms.CheckBox.1"
        Case "optionbutton"
            GetProgIdFromCtrlType = "Forms.OptionButton.1"
        Case "frame"
            GetProgIdFromCtrlType = "Forms.Frame.1"
        Case "multipage"
            GetProgIdFromCtrlType = "Forms.MultiPage.1"
        Case "tabstrip"
            GetProgIdFromCtrlType = "Forms.TabStrip.1"
        Case "scrollbar"
            GetProgIdFromCtrlType = "Forms.ScrollBar.1"
        Case "spinbutton"
            GetProgIdFromCtrlType = "Forms.SpinButton.1"
        Case "image"
            GetProgIdFromCtrlType = "Forms.Image.1"
        Case Else
            GetProgIdFromCtrlType = vbNullString
    End Select

End Function

Private Sub ApplyControlLayout(ByVal ctrl As Object, ByRef parts() As String)

    On Error Resume Next

    ctrl.Left = CDbl(parts(3))
    ctrl.Top = CDbl(parts(4))
    ctrl.Width = CDbl(parts(5))
    ctrl.Height = CDbl(parts(6))

    ctrl.caption = parts(2)

    If UBound(parts) >= 7 Then
        If parts(7) <> vbNullString Then ctrl.Font.Name = parts(7)
    End If

    If UBound(parts) >= 8 Then
        If parts(8) <> vbNullString Then ctrl.Font.Size = CDbl(parts(8))
    End If

    If UBound(parts) >= 9 Then
        If parts(9) <> vbNullString Then ctrl.Font.Bold = CBool(parts(9))
    End If

    If UBound(parts) >= 10 Then
        If parts(10) <> vbNullString Then ctrl.foreColor = CLng(parts(10))
    End If

    If UBound(parts) >= 11 Then
        If parts(11) <> vbNullString Then ctrl.backColor = CLng(parts(11))
    End If

    On Error GoTo 0

End Sub

' ==========================================================
'  UserForm 코드 / Document 모듈
' ==========================================================

Private Sub ImportFormCodeFromCls(ByVal vbComp As Object, ByVal folderPath As String)

    On Error GoTo Err_Handler

    ReplaceCodeModuleFromFile vbComp, folderPath & "\" & vbComp.Name & ".cls"
    Exit Sub

Err_Handler:
    AddProblem "UserForm 코드 " & vbComp.Name, Err.Number & ": " & Err.Description

End Sub

Private Sub ImportDocumentModuleFromCls(ByVal vbComp As Object, ByVal folderPath As String)

    On Error GoTo Err_Handler

    ReplaceCodeModuleFromFile vbComp, folderPath & "\" & vbComp.Name & ".cls"
    Exit Sub

Err_Handler:
    AddProblem "Document 코드 " & vbComp.Name, Err.Number & ": " & Err.Description

End Sub

' 구성 요소는 그대로 두고 코드 내용만 파일 내용으로 바꾼다
Private Sub ReplaceCodeModuleFromFile(ByVal vbComp As Object, ByVal filePath As String)

    Dim codeText As String
    Dim cm As Object
    Dim lineCount As Long

    If Dir$(filePath) = vbNullString Then Exit Sub

    codeText = ReadUtf8Text(filePath)
    codeText = RemoveVbeExportHeader(codeText)

    Set cm = vbComp.CodeModule

    lineCount = cm.CountOfLines
    If lineCount > 0 Then
        cm.DeleteLines 1, lineCount
    End If

    If Len(codeText) > 0 Then
        cm.InsertLines 1, codeText
    End If

End Sub

' ==========================================================
'  결과 보고
' ==========================================================

Private Sub AddProblem(ByVal target As String, ByVal description As String)

    m_problems.Add target & " → " & description
    Debug.Print "[가져오기] " & target & " → " & description

End Sub

Private Sub ShowImportResult(ByVal baseFolder As String)

    Dim message As String
    Dim i As Long

    If m_problems.Count = 0 Then
        MsgBox "가져오기 완료: " & vbCrLf & baseFolder, vbInformation
        Exit Sub
    End If

    message = "가져오기가 끝났지만 문제가 " & m_problems.Count & "건 있었습니다." & vbCrLf & _
              "일부만 반영되었을 수 있으니 백업과 비교해 확인해 주세요." & vbCrLf & vbCrLf

    For i = 1 To m_problems.Count
        If i > MAX_PROBLEMS_IN_MESSAGE Then
            message = message & "… 외 " & (m_problems.Count - MAX_PROBLEMS_IN_MESSAGE) & _
                      "건 (전체 목록은 VBE 직접 실행 창 참고)" & vbCrLf
            Exit For
        End If
        message = message & "- " & m_problems(i) & vbCrLf
    Next i

    MsgBox message, vbExclamation

End Sub

' ==========================================================
'  헬퍼
' ==========================================================

Private Function GetVBProjectSafe(ByVal wb As Workbook) As Object

    Dim vbproj As Object

    On Error Resume Next
    Set vbproj = wb.VBProject
    On Error GoTo 0

    If vbproj Is Nothing Then
        MsgBox "VBA 프로젝트에 접근할 수 없습니다." & vbCrLf & _
               "보안 센터에서 'VBA 프로젝트 개체 모델에 안전하게 액세스할 수 있음'을 켜 주세요.", vbExclamation
        Exit Function
    End If

    If vbproj.Protection = vbext_pp_locked Then
        MsgBox "VBA 프로젝트가 암호로 잠겨 있습니다. 잠금을 푼 뒤 다시 실행해 주세요.", vbExclamation
        Exit Function
    End If

    Set GetVBProjectSafe = vbproj

End Function

Private Function FindComponent(ByVal vbproj As Object, ByVal componentName As String) As Object

    On Error Resume Next
    Set FindComponent = vbproj.VBComponents(componentName)
    Err.Clear
    On Error GoTo 0

End Function

Private Function GetUnusedComponentName(ByVal vbproj As Object, ByVal prefix As String) As String

    Dim n As Long

    Do
        n = n + 1
        If FindComponent(vbproj, prefix & n) Is Nothing Then
            GetUnusedComponentName = prefix & n
            Exit Function
        End If
    Loop

End Function

Private Function GetDesignerSafe(ByVal vbComp As Object) As Object

    On Error Resume Next
    Set GetDesignerSafe = vbComp.designer
    Err.Clear
    On Error GoTo 0

End Function

Private Function GetDesignerControlSafe(ByVal designer As Object, ByVal ctrlName As String) As Object

    On Error Resume Next
    Set GetDesignerControlSafe = designer.Controls(ctrlName)
    Err.Clear
    On Error GoTo 0

End Function

Private Function AddDesignerControlSafe(ByVal designer As Object, ByVal progId As String, _
                                        ByVal ctrlName As String) As Object

    On Error Resume Next
    Set AddDesignerControlSafe = designer.Controls.Add(progId, ctrlName, True)
    Err.Clear
    On Error GoTo 0

End Function

Private Sub DeleteFileQuietly(ByVal filePath As String)

    On Error Resume Next
    If Dir$(filePath) <> vbNullString Then Kill filePath
    Err.Clear
    On Error GoTo 0

End Sub

Private Function GetNameWithoutExtension(ByVal fileName As String) As String

    Dim pos As Long

    pos = InStrRev(fileName, ".")

    If pos > 0 Then
        GetNameWithoutExtension = Left$(fileName, pos - 1)
    Else
        GetNameWithoutExtension = fileName
    End If

End Function

' ----------------------------------------------------------
'  CSV 전체를 해석해 행(String 배열)의 Collection 으로 반환
'
'  - 따옴표 밖의 CRLF / LF / CR 은 행의 끝
'  - 따옴표 안의 줄바꿈은 값의 일부로 그대로 유지
'  - "" 는 따옴표 1개
' ----------------------------------------------------------
Private Function ParseCsvRecords(ByVal allText As String) As Collection

    Dim records As Collection
    Dim fields() As String
    Dim fieldCount As Long
    Dim current As String
    Dim inQuote As Boolean
    Dim recordHasData As Boolean
    Dim textLength As Long
    Dim i As Long
    Dim ch As String

    Set records = New Collection
    ReDim fields(0)

    textLength = Len(allText)
    i = 1

    Do While i <= textLength
        ch = Mid$(allText, i, 1)

        If inQuote Then
            If ch = """" Then
                If i < textLength And Mid$(allText, i + 1, 1) = """" Then
                    current = current & """"
                    i = i + 1
                Else
                    inQuote = False
                End If
            Else
                current = current & ch
            End If

        Else
            Select Case ch
                Case """"
                    inQuote = True
                    recordHasData = True

                Case ","
                    fields(fieldCount) = current
                    fieldCount = fieldCount + 1
                    ReDim Preserve fields(fieldCount)
                    current = vbNullString
                    recordHasData = True

                Case vbCr, vbLf
                    If ch = vbCr And i < textLength Then
                        If Mid$(allText, i + 1, 1) = vbLf Then i = i + 1
                    End If

                    AddCsvRecord records, fields, fieldCount, current, recordHasData

                Case Else
                    current = current & ch
                    recordHasData = True
            End Select
        End If

        i = i + 1
    Loop

    AddCsvRecord records, fields, fieldCount, current, recordHasData

    Set ParseCsvRecords = records

End Function

Private Sub AddCsvRecord(ByVal records As Collection, ByRef fields() As String, ByRef fieldCount As Long, _
                         ByRef current As String, ByRef recordHasData As Boolean)

    If recordHasData Then
        fields(fieldCount) = current
        records.Add fields
    End If

    ReDim fields(0)
    fieldCount = 0
    current = vbNullString
    recordHasData = False

End Sub

