Private/classes.ps1

#region XML-processing Classes

# The following classes process different kinds of XML files found
# in an Office Open XML document.

Class xmlFile {
    [string] $ns = ""
    [hashtable] $nsm = @{}

    [xml] $xdoc
    [bool] hidden $isvalid = $false

    [void] CheckValidity() {
        If (-not $this.isValid) {
            Throw "The schema is not valid."
        }
    }

    [void] hidden setns([string]$rootNsUrn) {
        $this.ns = $rootNsUrn
        $this.nsm["x"] = $rootNsUrn         
    }

    [void] ValidateSchema() {
        $this.isvalid = $true
    }

    [void] Save([string] $filePath) {
        $this.CheckValidity()
        Set-Content -LiteralPath $filePath ($this.xdoc.InnerXml) -ErrorAction Stop
    }

    [void] SaveFormatted([string] $filePath) {
        $this.CheckValidity()
        $this.xdoc.Save($filePath)
    }
 
    xmlfile([string] $filePath, [string] $rootNsUrn) {
        $this.setns($rootNsUrn)
        $this.xdoc = [xml] (Get-Content $filePath -ErrorAction Stop)
        $this.ValidateSchema()
    }

    xmlfile([string] $rootNsUrn) {
        $this.setns($rootNsUrn)
        $this.xdoc = [xml]"<root xmlns='$rootNsUrn'></root>"
        $this.isvalid = $true
    }

    xmlfile() {
        $this.isvalid = $false
    }
}

Class contentTypesXMLFile : xmlFile {
    static $ns = "http://schemas.openxmlformats.org/package/2006/content-types"

    [bool] HasPngNode() {
        $pngnode = ( `
                $this.xdoc | `
                Select-Xml "/x:Types/x:Default[Extension='png']" -Namespace $this.nsm `
        )
        Return ($null -ne $pngnode )
    }

    [void] AddPngNode() {
        If ($this.HasPngNode()) {
            Return
        }
        
        $newNode = $this.xdoc.CreateElement("Default", [contentTypesXMLFile]::ns)
        $newNode.SetAttributeNode("Extension", "").Value = "png"
        $newNode.SetAttributeNode("ContentType", "").Value = "image/png"
        $this.xdoc.DocumentElement.AppendChild($newNode)
    }

    contentTypesXMLFile([string] $filePath) : base() {
        $this.setns([contentTypesXMLFile]::ns)
        # Content Types file has square brackets in the name
        # Literalpath _must_ be used.
        $this.xdoc = [xml] (Get-Content -LiteralPath $filePath -ErrorAction Stop)
        $this.ValidateSchema()
    }
}

Class relsXMLFile : xmlFile {
    static $ns = "http://schemas.openxmlformats.org/package/2006/relationships"

    [void] ValidateSchema() {
        $rootnode = $this.xdoc | Select-Xml "/x:Relationships" -Namespace $this.nsm
        If ($null -ne $rootnode) {
            $this.isvalid = $true
        }
        Else {
            $this.isvalid = $false
        }
    }

    [void] AddRel([string]$id, [string]$type, [string] $target) {
        $newNode = $this.xdoc.CreateElement("Relationship", $this.ns)
        $newNode.SetAttributeNode("Id", "").Value = $id
        $newNode.SetAttributeNode("Type", "").Value = $type
        $newNode.SetAttributeNode("Target", "").Value = $target
        
        $this.xdoc.DocumentElement.AppendChild($newNode)
    }

    [void] AddImageRel([string]$id, [string]$imagePath) {
        $this.AddRel( `
                $id, `
                "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", `
                $imagePath `
        )
    }

    [void] AddCustomUIRel([string] $id, [string]$customUIPath) {
        $this.AddRel( `
                $id, `
                "http://schemas.microsoft.com/office/2007/relationships/ui/extensibility", `
                $customUIPath `
        )
    }

    relsXMLFile([string] $filePath) : base($filePath, [relsXMLFile]::ns ) {

    }

    relsXMLFile() : base() {
        $relsns = [relsXMLFile]::ns
        $this.setns($relsns)
        $this.xdoc = [xml]"<?xml version=`"1.0`" encoding=`"UTF-8`" standalone=`"yes`"?>`n<Relationships xmlns='$relsns'></Relationships>"
        $this.isValid = $true
    }
}

Class customUIXMLFile : xmlFile {
    static $ns = "http://schemas.microsoft.com/office/2009/07/customui"

    [void] ValidateSchema() {
        $this.isValid = $false

        $ribbonElement = $this.xdoc | `
            Select-Xml  "/x:customUI/x:ribbon[@startFromScratch]" `
            -Namespace $this.nsm

        If ($null -eq $ribbonElement ) {
            Throw "Wrong schema found."
        }
    
        # Validate that there is only one ribbon element in the right place
        If ($ribbonElement.Count -ne 1) {
            Throw "There can be only one ribbon element."
        }
    
        # Validate that the ribbon element does not start from scratch
        If ($ribbonElement.Node.startFromScratch -ne "false") {
            Throw "Custom UIs are not allowed to have a from-scratch ribbon."
        }

        $this.isValid = $true
    }

    [string] InnerXml() {
        $this.CheckValidity()
        Return $this.xdoc.InnerXml
    }

    [object] GetTabs() {
        $this.CheckValidity()
        $tabs = $this.xdoc | Select-Xml `
            "//x:tabs/x:tab" `
            -Namespace $this.nsm
        If ($null -eq $tabs) {
            Return @()
        }
        Return $tabs
    }

    [object] GetButtons() {
        $this.CheckValidity()
        $buttons = $this.xdoc | Select-Xml `
            "/x:customUI/x:ribbon/x:tabs//x:button" `
            -Namespace $this.nsm
        If ($null -eq $buttons) {
            Return @()
        }
        Return $buttons
    }

    [bool] Merge([customUIXMLFile] $otherfile) {
        $somethingAdded = $false

        $otherfile.GetTabs() | ForEach-Object {
            If ($null -ne $_) {
                $addedForTab = $this.processTab($_.Node)
                $somethingAdded = $somethingAdded -or $addedForTab
            }
        }

        If ($somethingAdded) {
            $this.ValidateSchema()
        }

        Return $somethingAdded
    }

    [bool] hidden processElement($currentElement, $currentElementQuery, $parentElementQuery) {        
        $existingElement = $this.xdoc | `
            Select-Xml $currentElementQuery -Namespace $this.nsm
    
        If ($null -eq $existingElement) {
            $inode = $this.xdoc.ImportNode($currentElement, $true)
            $parentnode = ( `
                    $this.xdoc | `
                    Select-Xml $parentElementQuery -Namespace $this.nsm `
            ).Node
            $parentnode.AppendChild($inode)
    
            Return $true
        }
    
        Return $false
    }

    [bool] hidden processTab($currentTab) {
        $currentTabQuery = "//x:tabs/x:tab[@idMso='$($currentTab.idMso)' or @id='$($currentTab.id)']"
    
        $tabAdded = $this.processElement( `
                $currentTab, `
                $currentTabQuery, `
                "//x:ribbon/x:tabs" `
        )
        
        If (-not $tabAdded) {
            # If the tab is already there, iterate groups
            $groupsOrButtonsAdded = $false
            $groups = ( `
                    $currentTab | `
                    Select-Xml "//x:tab/x:group" -Namespace $this.nsm `
            )
            
            # Write-Verbose "Adding $($groups.Count) groups in tab $($currentTab.idMso)"
            $groups | ForEach-Object {
                $somethingAdded = $this.processTabGroup($_.Node, $currentTabQuery)
                $groupsOrButtonsAdded = $groupsOrButtonsAdded -or $somethingAdded
            }

            Return $groupsOrButtonsAdded
        }

        # Write-Verbose "Added tab $($currentTab.idMso)"
        Return $tabAdded
    }

    [bool] hidden processTabGroup($currentGroup, $currentTabQuery) {
        $currentGroupQuery = "$currentTabQuery/x:group[@id='$($currentGroup.id)']"
    
        $groupAdded = $this.processElement( `
                $currentGroup, `
                $currentGroupQuery, `
                $currentTabQuery `
        )
        
        If (-not $groupAdded) {
            # If group is already there, iterate buttons
            $buttonsAdded = $false
            $buttons = ( `
                    $currentGroup | `
                    Select-Xml "//x:group/x:button" -Namespace $this.nsm `
            )

            # Write-Verbose "Adding $($buttons.Count) buttons in group $($currentGroup.id)"
            $buttons | ForEach-Object {
                # Write-Verbose "Adding button $($_.Node.id)"
                $buttonAdded = $this.processButton($_.Node, $currentGroupQuery)
                $buttonsAdded = $buttonsAdded -or $buttonAdded
            }

            Return $buttonsAdded
        }

        # Write-Verbose "Added group $($currentGroup.id)"
        Return $groupAdded
    }

    [bool] hidden processButton($currentButton, $currentGroupQuery) {
        $currentButtonQuery = "$currentGroupQuery/x:button[@id='$($currentButton.id)']"
        
        $buttonAdded = $this.processElement( `
                $currentButton, `
                $currentButtonQuery, `
                $currentGroupQuery `
        )

        If (-not $buttonAdded) {
            # Current button should NOT exist in merged
            Throw "Duplicate button id: $($currentButton.id)"
        }

        # Write-Verbose "Added button $($currentButton.id)"
        Return $buttonAdded
    }

    customUIXMLFile([string] $filePath) : base($filePath, [customUIXMLFile]::ns ) {}

    customUIXMLFile() : base() {
        $this.setns([customUIXMLFile]::ns)
        $this.xdoc = [xml]'<customUI xmlns="http://schemas.microsoft.com/office/2009/07/customui"><ribbon startFromScratch="false"><tabs></tabs></ribbon></customUI>'
        $this.isValid = $true
    }
}

#endregion

#region MVPackage

Class MVPackage {
    [string] $Name
    [string] $Path

    [customUIXMLFile] hidden $cui
    [string] hidden $validationErr

    [string] CustomUIPath() {
        Return (Join-Path $this.Path "\CustomUI\customUI14.xml")
    }

    [string] CustomUIDir() {
        Return (Join-Path $this.Path "\CustomUI")
    }

    [bool] HasCustomUI() {
        Return (Test-Path $this.CustomUIPath())
    }

    [customUIXMLFile] CustomUI() {
        If (-not $this.HasCustomUI()) {
            Return $null
        }

        If ($null -eq $this.cui) {
            $this.cui = [customUIXMLFile]::New($this.CustomUIPath())
        }

        Return $this.cui
    }

    [void] Ensure() {
        [MVLibrary]::EnsureDirectory($this.Path)

        [MVLibrary]::EnsureDirectory((Join-Path $this.Path "Tests"))
    }

    [void] AddCustomUI() {
        If ($this.HasCustomUI()) {
            Throw "Package $($this.Name) already has custom UI."
        }

        [MVLibrary]::EnsureDirectory($this.CustomUIDir())
        
        $newcui = [customUIXMLFile]::New()
        $newcui.Save($this.CustomUIPath())

        $this.cui = $newcui
    }

    [bool] ValidateCustomUI() {
        $result = $true

        # Validate that all button image files are present
        $currentcui = $this.CustomUI()
        $buttons = $currentcui.GetButtons()

        foreach ($item in $buttons) {
            $imagename = "$($item.Node.image)"
            If ("" -ne $imagename) {
                $imagepath = Join-Path $this.CustomUIDir() "$imagename.png"
                If (-not (Test-Path $imagepath)) {
                    $this.validationErr = "Image $imagepath not present in $($this.Name)."
                    $result = $false
                    break
                }
            }
        }

        Return $result
    }

    [string] ValidationError() {
        Return $this.validationErr
    }

    [void] Delete() {
        Remove-Item $this.Path -Force -Recurse -ErrorAction Stop
    }

    MVPackage($packageName, $packagePath) {
        $this.Name = $packageName
        $this.Path = $packagePath
    }
}

#endregion

#region MVBuilder

Class MVBuilder {
    [string] $Name
    [string] $Type
    [string] $OutPath

    # Will be overridden in application-specific builders
    [string] OfficeApplication() {
        Return "None"
    }

    # Will be overridden in application-specific builders
    [string] FileExtension([string] $buildType) {
        Return ".none"
    }

    [string] TargetPathNoExtension() {
        Return  Join-Path $this.OutPath $this.Name
    }

    [string] TargetPath() {
        $buildext = $this.FileExtension($this.Type)
        $targetPath = Join-Path $this.OutPath "$($this.Name).$buildext"
        Return $targetPath
    }

    [string] TargetUIDir() {
        Return (Join-Path $this.OutPath "$($this.Name).UI")
    }

    [void] Delete() {
        $buildfilepath = $this.TargetPath()
        $buildexpandeddir = "$buildfilepath.d"
        $builduidir = $this.TargetUIDir()
        Remove-Item $builduidir -Recurse -Force -ErrorAction Ignore
        Remove-Item $buildexpandeddir -Recurse -Force -ErrorAction Ignore
        Remove-Item $buildfilepath -Force -ErrorAction Ignore
    }

    [void] Build([MVPackage[]]$Packages) {
        If ($Packages.Count -eq 0) {
            Return
        }

        # Merge Office document
        $this.MergeOfficeDocument($Packages)

        # Merge Custom UI
        $cuiMerged = $this.MergeCustomUI($Packages)

        # Expand Office document, add Custom UI, and zip it
        # up again.
        If (-not(Test-Path $this.TargetPath() -PathType Leaf)) {
            Write-Host "Could not find built Office file. Skipping UI addition..."
            Return
        }

        If (-not $cuiMerged) {
            Write-Host "Skipping UI addition..."
            Return
        }

        $this.AddUIToDocument($this.TargetPath(), $this.TargetUIDir())
    }

    [void] MergeVBAModules([MVPackage[]]$Packages, $vbp) {
        $Packages | ForEach-Object {
            Write-Host " Processing package $($_.Name)..."

            $packagepathspec = "$($_.Path)\*"
            $basfiles = (Get-ChildItem -Path $packagepathspec -Include *.bas, *.frm, *.cls)
            $basfiles | ForEach-Object {
                $newComponent = $vbp.VBComponents.Import($_.FullName)
                Write-Host " Merged $($newComponent.Name)"     
            }
        }
    }

    # Will be overridden in application-specific builders
    [void] MergeOfficeDocument([MVPackage[]]$Packages) {
        Write-Host "Skipping merging Office $($this.Type)."
    }

    [bool] MergeCustomUI([MVPackage[]] $Packages) {
        Write-Host "Merging Custom UI..."        
        
        # Initialize Custom UI Directory and object
        $mergedCuiDir = $this.TargetUIDir()
        $mergedImageDir = (Join-Path $mergedCuiDir "images")
        Remove-Item $mergedCuiDir -Recurse -Force -ErrorAction Ignore
        $newcui = [customUIXMLFile]::New()
        $cuiMerged = $false
        
        foreach ($currentPackage in $Packages) {   
            # Merge Custom CUI
            If ($currentPackage.HasCustomUI() ) {
                If (-not $currentPackage.ValidateCustomUI()) {
                    Throw "Package $($currentPackage.Name) could not be merged: $($currentPackage.validationErr)"
                }

                Write-Host " Processing package $($currentPackage.Name)..."

                $packageCuiMerged = $newcui.Merge($currentPackage.CustomUI())
                $cuiMerged = $cuiMerged -or $packageCuiMerged
            
                If ($packageCuiMerged) {
                    [MVLibrary]::EnsureDirectory($mergedImageDir)
                    # Copy images
                    Copy-Item "$($currentPackage.Path)\CustomUI\*png" -Destination $mergedImageDir -Force
                }
            }
        }

        # If any Custom UI merging happened, save customUI
        # file and build rels file
        If ($cuiMerged) {
            [MVLibrary]::EnsureDirectory($mergedCuiDir)
            $newcuipath = Join-Path $this.TargetUIDir() "customUI14.xml"
            $newcui.Save($newcuipath)
            
            # Set up rels for copied images
            $imageCount = (Get-ChildItem $mergedImageDir -Filter "*.png" -ErrorAction Ignore).Count
            If ($imageCount -gt 0) {
                Write-Host " Adding image rels file..."

                $relsdir = Join-Path $mergedCuiDir "_rels"
                $relsfilepath = Join-Path $relsdir "customUI14.xml.rels"
            
                [MVLibrary]::EnsureDirectory($relsdir)
                $relsfile = [relsXMLFile]::New()
            
                $newcui.GetButtons() | ForEach-Object {
                    $currentButton = $_.Node
                    If (-not [string]::IsNullOrEmpty($currentButton.image)) {
                        $relsfile.AddImageRel($currentButton.image, "images/$($currentButton.image).png")
                    }
                }
            
                $relsfile.Save($relsfilepath)
            }

            Write-Host "Custom UI done."
        }
        Else {
            Write-Host "No custom UI present."
        }

        Return $cuiMerged
    }

    [void] AddUIToDocument([string]$officefilename, [string]$cuidir) {
        Write-Host "Adding UI to Office $($this.Type)..."
        $officezipfilename = "$officefilename.zip"
        $expanddir = "$officefilename.d"
    
        Remove-Item $officezipfilename -Force -ErrorAction Ignore
        Remove-Item $expanddir -Recurse -Force -ErrorAction Ignore
        
        # The PowerShell Expand-Archive cmdlet only works if the
        # file extension is .zip. So rename the office document,
        # expand to a directory, and delete the document
        Move-Item $officefilename $officezipfilename
        Expand-Archive $officezipfilename -DestinationPath $expanddir -ErrorAction Stop
        Remove-Item $officezipfilename -Force -ErrorAction Ignore
        
        # Move the previously consolidated custom UI directory
        # inside the expanded Office document directory
        Move-Item $cuiDir -Destination "$expanddir\customUI"
    
        # Edit the main rels file of the OpenOfficeXML document
        # to include the custom UI
        $relsFilePath = "$expanddir\_rels\.rels"
        $relsFile = [relsXMLFile]::New($relsFilePath)
        $newRelId = "R" + [string](Get-Random)
        $relsFile.AddCustomUIRel($newRelId, "/customUI/customUI14.xml")
        $relsFile.Save($relsFilePath)
    
        # Edit content-types file of the OpenOfficeXML document
        # to include png files if not already present.
        # Square brackets are tricky in PowerShell, so all operations
        # on this file will use the -LiteralPath parameter.
        $ctFilePath = "$expanddir\[Content_Types].xml"
        $ctFile = [contentTypesXMLFile]::New($ctFilePath)
        $ctFile.AddPngNode()
        $ctFile.Save($ctFilePath)
        
        # Re-compress the expanded Office document directory
        # with the custom UI files included, to a .zip file
        # because the Compress-Archive cmdlet will only zip to
        # that kind of file. Note that the _contents_ of the
        # directory are zipped. Office document files are zip
        # files with files and directories directly under the
        # root.
        Compress-Archive "$expanddir\*" $officezipfilename -ErrorAction Stop
    
        # Rename it to the original name.
        Move-Item $officezipfilename $officefilename
    
        # Delete the expanded directory.
        Remove-Item $expanddir -Recurse -Force -ErrorAction Ignore
        
        Write-Host "Adding UI done."
    }


    MVBuilder([string] $buildName, [string] $buildType, [string] $buildParentDir) {
        $this.Name = $buildName
        $this.Type = $buildType
        $this.OutPath = $buildParentDir
    }
}

Class MVBuilderPPT : MVBuilder {
    static $BuildExtensions = @{
        "AddIn"    = "ppam";
        "Document" = "pptm"
    }

    # Override for PowerPoint
    [string] OfficeApplication() {
        Return "PowerPoint"
    }

    [string] FileExtension([string] $buildType) {
        Return [MVBuilderPPT]::BuildExtensions[$buildType]
    }

    [void] MergeOfficeDocument([MVPackage[]]$Packages) {
        Write-Host "Merging PowerPoint $($this.Type)..."

        $ppa = $null
        $newPpt = $null
        
        # Create PowerPoint object
        Try {
            $ppa = New-Object -ComObject PowerPoint.Application
        
            $newPpt = $ppa.Presentations.Add($false)
        }
        Catch {
            Throw "PowerPoint does not seem to be available."
        }
        
        Try {
            # Try to get the VBA project object.
            # If VBA Object model access is not trusted, $vbp
            # will contain $null
            $vbp = $newPpt.VBProject
            
            If ($null -eq $vbp) {
                Throw "Access to VBA Object Model not trusted. Please check the Trust Access to the VBA Object model checkbox in the PowerPoint Trust Centre."
            }
    
            $this.MergeVBAModules($Packages, $vbp)

            If ($this.Type -eq 'Document') {
                # 25 = ppSaveAsOpenXMLPresentationMacroEnabled
                $newPpt.SaveAs($this.TargetPathNoExtension(), 25, $false)
            }
            Else {
                # 30 = ppSaveAsOpenXMLAddin
                $newPpt.SaveAs($this.TargetPathNoExtension(), 30, $false)
            }

            Write-Host "PowerPoint $($this.Type) done."
        }
        Finally {
            $newPpt.Close()
            $newPpt = $null
    
            $ppa.Quit()
            $ppa = $null
        }
    }

    MVBuilderPPT([string] $buildName, [string] $buildType, [string] $buildParentDir) :base($buildName, $buildType, $buildParentDir) {

    }
}

Class MVBuilderExcel : MVBuilder {
    static $BuildExtensions = @{
        "AddIn"    = "xlam";
        "Document" = "xlsm"
    }

    # Override for Excel
    [string] OfficeApplication() {
        Return "Excel"
    }

    [string] FileExtension([string] $buildType) {
        Return [MVBuilderExcel]::BuildExtensions[$buildType]
    }

    [void] MergeOfficeDocument([MVPackage[]]$Packages) {
        Write-Host "Merging Excel $($this.Type)..."

        $xla = $null
        $newWkbk = $null
        
        # Create Excel object
        Try {
            $xla = New-Object -ComObject Excel.Application
        
            $newWkbk = $xla.Workbooks.Add()
        }
        Catch {
            Throw "Excel does not seem to be available."
        }
        
        Try {
            # Try to get the VBA project object.
            # If VBA Object model is not trusted, $vbp
            # will contain $null
            $vbp = $newWkbk.VBProject
            
            If ($null -eq $vbp) {
                Throw "Access to VBA Object Model not trusted. Please check the Trust Access to the VBA Object model checkbox in the Macro Settings section of the Excel Trust Centre."
            }

            $this.MergeVBAModules($Packages, $vbp)
        
            If ($this.Type -eq 'Document') {
                # 52 = xlOpenXMLWorkbookMacroEnabled
                $newWkbk.SaveAs($this.TargetPathNoExtension(), 52)
            }
            Else {
                # 55 = xlOpenXMLAddIn
                $newWkbk.SaveAs($this.TargetPathNoExtension(), 55)
            }

            Write-Host "Excel $($this.Type) done."
        }
        Finally {
            $newWkbk.Close()
            $newWkbk = $null
    
            $xla.Quit()
            $xla = $null
        }
    }

    MVBuilderExcel([string] $buildName, [string] $buildType, [string] $buildParentDir) :base($buildName, $buildType, $buildParentDir) {

    }
}

Class MVBuilderWord : MVBuilder {
    static $BuildExtensions = @{
        "AddIn"    = "";
        "Document" = "docm"
    }

    # Override for Excel
    [string] OfficeApplication() {
        Return "Word"
    }

    [string] FileExtension([string] $buildType) {
        Return [MVBuilderWord]::BuildExtensions[$buildType]
    }

    [void] MergeOfficeDocument([MVPackage[]]$Packages) {
        Write-Host "Merging Word $($this.Type)..."

        $wda = $null
        $newDoc = $null
        
        # Create Word object
        Try {
            $wda = New-Object -ComObject Word.Application
        
            $newDoc = $wda.Documents.Add()
        }
        Catch {
            Throw "Word does not seem to be available."
        }
        
        Try {
            # Try to get the VBA project object.
            # If VBA Object model is not trusted, $vbp
            # will contain $null
            $vbp = $newDoc.VBProject
            
            If ($null -eq $vbp) {
                Throw "Access to VBA Object Model not trusted. Please check the Trust Access to the VBA Object model checkbox in the Word Trust Centre."
            }

            $this.MergeVBAModules($Packages, $vbp)
        
            If ($this.Type -eq 'Document') {
                # 13 = wdFormatXMLDocumentMacroEnabled
                $newDoc.SaveAs2($this.TargetPathNoExtension(), 13)
            }
            Else {
                # 15 = wdFormatXMLTemplateMacroEnabled
                $newDoc.SaveAs2($this.TargetPathNoExtension(), 15)
            }

            Write-Host "Word $($this.Type) done."
        }
        Finally {
            $newDoc.Close()
            $newDoc = $null
    
            $wda.Quit()
            $wda = $null
        }
    }

    MVBuilderWord([string] $buildName, [string] $buildType, [string] $buildParentDir) :base($buildName, $buildType, $buildParentDir) {

    }
}
#endregion

#region MVLibrary

Class MVLibrary {
    [string[]] static $SupportedApplications = "none", "powerpoint", "excel", "word"
    [string] $Name
    [string] $Application

    [void] static EnsureDirectory([string]$OutPath) {
        If ("" -eq $OutPath) {
            Throw "Empty Path."
        }
    
        $outdirexists = (Test-Path -PathType Container $OutPath)
        If ($outdirexists -eq $false) {
            New-Item -ItemType Directory -Force -Path $OutPath -ErrorAction Stop
        }
    }

    [System.Management.Automation.ErrorRecord] static CmdLetError($err) {
        If ($_ -is [System.Management.Automation.ErrorRecord]) {
            return $err
        }

        return $PSCmdlet.WriteError( `
                [System.Management.Automation.ErrorRecord]::New( `
                    $err, `
                    "MOVBA.Error", `
                    [System.Management.Automation.ErrorCategory]::NotSpecified, `
                    $null `
            ) `
        )
    }
    
    [void] static ThrowDirectoryException() {
        Throw "This CmdLet can only be used in a project root directory."
    }

    [MVLibrary] static GetProjectConfig() {       
        # Check if the packages subdirectory is present
        If (-not ( `
                (Test-Path "$pwd\packages" -PathType Container ) `
                    -and 
                (Test-Path "$pwd\library.json" -PathType Leaf)
            )) {
            [MVLibrary]::ThrowDirectoryException()
        }

        $appconfig = ConvertFrom-Json (Get-Content -Raw "$pwd\library.json" -ErrorAction Ignore)
        If ($null -eq $appconfig) {
            [MVLibrary]::ThrowDirectoryException()
        }

        $lib = [MVLibrary]::New($appconfig.Name, $appconfig.Application)

        Return $lib
    }

    [void] static TestCurrentDirectory() {
        If ($null -eq [MVLibrary]::GetProjectConfig()) {
            [MVLibrary]::ThrowDirectoryException()
        }
    }

    [bool] static TestOfficeApplication([string] $Application) {
        If (-not ( `
                    [MVLibrary]::SupportedApplications.Contains( `
                        $Application.ToLowerInvariant() `
                ) `
            ) `
        ) {
            Throw "Application $Application not currently supported."
        }

        Return $true
    }

    [MVBuilder] static GetBuilder([string] $Name, [string] $Type, [string] $Application) {
        $result = $null
        Switch ($Application) {
            "none" { 
                [MVLibrary]::EnsureDirectory("$pwd\out")
                $result = [MVBuilder]::New($Name, $Type, "$pwd\out")
                Break
            }
            "powerpoint" { 
                [MVLibrary]::EnsureDirectory("$pwd\out")
                $result = [MVBuilderPPT]::New($Name, $Type, "$pwd\out")
                Break
            }
            "excel" {
                [MVLibrary]::EnsureDirectory("$pwd\out")
                $result = [MVBuilderExcel]::New($Name, $Type, "$pwd\out")
                Break
            }
            "word" {
                [MVLibrary]::EnsureDirectory("$pwd\out")
                $result = [MVBuilderWord]::New($Name, $Type, "$pwd\out")
                Break
            }
            Default { 
                Throw "Application $Application not currently supported."
            }
        }
        Return $result
    }

    [MVLibrary] static Create([string] $Name, [string] $Application) {
        [MVLibrary]::TestOfficeApplication($Application)

        $projectDir = (Join-Path $pwd $Name) 
        If (Test-Path $projectDir) {
            Throw "Directory '$Name' already exists."
        }

        New-Item $projectDir -ItemType Directory

        Push-Location $projectDir

        $result = [MVLibrary]::Init($Name, $Application)

        Pop-Location

        return $result
    }

    [MVLibrary] static Init([string] $Name, [string] $Application) {
        [MVLibrary]::TestOfficeApplication($Application)


        If (Test-Path "$pwd\library.json" -PathType Leaf) {
            throw "This directory is already a library."
        }

        $appconfig = [PSCustomObject] @{"Name" = $Name; "Application" = $Application }
        $appconfigJSON = ConvertTo-Json $appconfig
        Set-Content -Value $appconfigJSON -Path "$pwd\library.json" -ErrorAction Stop

        # Create packages subdirectory
        If (-not (Test-Path "$pwd\packages" -PathType Container)) {
            New-Item "$pwd\packages" -ItemType Directory
        }

        # Create out subdirectory
        If (-not (Test-Path "$pwd\out" -PathType Container)) {
            New-Item "$pwd\out" -ItemType Directory
        }

        If (-not (Test-Path "$pwd\.gitignore" -PathType Leaf)) {
            Set-Content "$pwd\.gitignore" (@(
                    "# Office temporary files"
                    "*.tmp"
                    "~$*.ppt*"
                    "~$*.doc*"
                    "~$*.xls*"
                    ""
                    "# output directories"
                    "out/"
                ) -join "`n") 
        } else {
            Add-Content "$pwd\.gitignore" (@(
                ""
                "# Office temporary files"
                "*.tmp"
                "~$*.ppt*"
                "~$*.doc*"
                "~$*.xls*"
                ""
                "# output directories"
                "out/"
            ) -join "`n")
        }

        $result = [MVLibrary]::New($appconfig.Name, $appconfig.Application)

        Write-Host "$($appconfig.Application) project initialised."

        return $result
    }

    [void] static Build([string] $Name, [string] $Type, [MVPackage[]]$Packages) {
        $appconfig = [MVLibrary]::GetProjectConfig()
        If ($null -eq $appconfig) {
            [MVLibrary]::ThrowDirectoryException()
        }

        [MVLibrary]::TestOfficeApplication($appconfig.Application)

        $builder = [MVLibrary]::GetBuilder($Name, $Type, $appconfig.Application)

        $builder.Build($Packages)
    }

    [MVPackage[]] static GetPackages([string[]] $PackageNames) {
        [MVLibrary]::TestCurrentDirectory()

        $packageDirQuery = Get-ChildItem -Path "$pwd\packages" -Directory
        If ($PackageNames.Count -gt 0) {
            $pnfc = $PackageNames | ForEach-Object { $_.ToLowerInvariant() }
            $packageDirQuery = $packageDirQuery | Where-Object { $pnfc.Contains($_.Name.ToLowerInvariant()) }
        }

        $packageDirQuery = $packageDirQuery | ForEach-Object { [MVPackage]::New($_.Name, $_.FullName) }

        Return $packageDirQuery
    }

    [MVPackage[]] static GetAllPackages() {
        Return [MVLibrary]::GetPackages(@())
    }

    [MVPackage] static GetPackage([string] $PackageName) {
        [MVLibrary]::TestCurrentDirectory()

        $packageDirQuery = Get-ChildItem -Path "$pwd\packages" -Directory | `
            Where-Object { $_.Name -eq $PackageName }

        $result = $packageDirQuery | ForEach-Object { [MVPackage]::New($_.Name, $_.FullName) }

        Return $result
    }

    [MVPackage] static NewPackage([string]$PackageName, [bool]$HasCustomUI) {
        [MVLibrary]::TestCurrentDirectory()

        $oldpackage = [MVLibrary]::GetPackage($PackageName)
        If ($null -ne $oldpackage) {
            Throw "Package $PackageName already exists."
        }

        # TODO: Consider switching to PS 6 minimum
        $newPackagePath = Join-Path (Join-Path "$pwd" "packages") "$PackageName"
        $newPackage = [MVPackage]::New($PackageName, $newPackagePath)
        $newPackage.Ensure()
        If ($HasCustomUI) {
            $newPackage.AddCustomUI()
        }

        Return $newPackage
    }

    [void] static RemovePackage([string]$PackageName) {
        [MVLibrary]::TestCurrentDirectory()

        $oldpackage = [MVLibrary]::GetPackage($PackageName)
        If ($null -eq $oldPackage) {
            Throw "Package $PackageName does not exist."
        }
    
        $oldPackage.Delete()    
    }

    MVLibrary([string] $libName, [string] $libApplication) {
        $this.Name = $libName
        $this.Application = $libApplication
    }
}

#endregion