Bicep.psm1

#region 10.BicepDiagnosticLevel

enum BicepDiagnosticLevel {
    Off
    Info
    Warning
    Error
}
#endregion 10.BicepDiagnosticLevel

#region 20.BicepDiagnosticEntry

class BicepDiagnosticEntry {
    [string] $LocalPath
    [int[]] $Position
    [BicepDiagnosticLevel] $Level
    [string] $Code
    [string] $Message

    BicepDiagnosticEntry ([object]$Entry) {
        if($Entry.pstypenames[0] -ne 'BicepNet.Core.DiagnosticEntry') {
            throw "Requires type 'BicepNet.Core.DiagnosticEntry'"
        }
        $this.LocalPath = $Entry.LocalPath
        $this.Position = $Entry.Position[0], $Entry.Position[1]
        $this.Level = $Entry.Level.ToString()
        $this.Code = $Entry.Code
        $this.Message = $Entry.Message
    }
}
#endregion 20.BicepDiagnosticEntry

#region BicepTypesCompleters

class BicepResourceProviderCompleter : System.Management.Automation.IArgumentCompleter{
    [System.Collections.Generic.IEnumerable[System.Management.Automation.CompletionResult]] CompleteArgument(
        [string] $CommandName,
        [string] $ParameterName,
        [string] $wordToComplete,
        [System.Management.Automation.Language.CommandAst] $CommandAst,
        [Collections.IDictionary] $fakeBoundParameters
    )
    {
        [array]$ResourceProviders = (GetBicepTypes).ResourceProvider | Where-Object { $_ -like "$wordToComplete*" } | Sort-Object -Unique
        
        $list = [System.Collections.Generic.List[System.Management.Automation.CompletionResult]]::new()
        
        foreach ($ResourceProvider in $ResourceProviders) {
            $CompletionText = $ResourceProvider
            $ListItemText   = $ResourceProvider
            $ResultType     = [System.Management.Automation.CompletionResultType]::ParameterValue
            $ToolTip        = $ResourceProvider
  
            $obj = [System.Management.Automation.CompletionResult]::new($CompletionText, $ListItemText, $ResultType, $Tooltip)
            $list.add($obj)
        }

        return $list
        
    }
 }

 class BicepResourceCompleter : System.Management.Automation.IArgumentCompleter{
    [System.Collections.Generic.IEnumerable[System.Management.Automation.CompletionResult]] CompleteArgument(
        [string] $CommandName,
        [string] $ParameterName,
        [string] $wordToComplete,
        [System.Management.Automation.Language.CommandAst] $CommandAst,
        [Collections.IDictionary] $fakeBoundParameters
    )
    {
        if ($fakeBoundParameters.ContainsKey('ResourceProvider')) {
            [array]$Resources = GetBicepTypes | Where-Object {
                $_.ResourceProvider -eq $fakeBoundParameters.ResourceProvider -and 
                $_.Resource -like "$wordToComplete*"
            } | Select-Object -ExpandProperty Resource -Unique | Sort-Object
        }
        else {
            [array]$Resources = (GetBicepTypes).Resource | Where-Object { $_ -like "$wordToComplete*" } | Sort-Object -Unique
        }
        
        $list = [System.Collections.Generic.List[System.Management.Automation.CompletionResult]]::new()
        
        foreach ($Resource in $Resources) {
            $CompletionText = $Resource
            $ListItemText   = $Resource
            $ResultType     = [System.Management.Automation.CompletionResultType]::ParameterValue
            
            $ToolTip = '{0}/{1}' -f $fakeBoundParameters.ResourceProvider, $Resource
            $ToolTip = $ToolTip.TrimEnd('/')
  
            $obj = [System.Management.Automation.CompletionResult]::new($CompletionText, $ListItemText, $ResultType, $Tooltip)
            $list.add($obj)
        }

        return $list
        
    }
 }

 class BicepResourceChildCompleter : System.Management.Automation.IArgumentCompleter{
    [System.Collections.Generic.IEnumerable[System.Management.Automation.CompletionResult]] CompleteArgument(
        [string] $CommandName,
        [string] $ParameterName,
        [string] $wordToComplete,
        [System.Management.Automation.Language.CommandAst] $CommandAst,
        [Collections.IDictionary] $fakeBoundParameters
    )
    {
        if ($fakeBoundParameters.ContainsKey('ResourceProvider') -and $fakeBoundParameters.ContainsKey('Resource')) {
            $Children = GetBicepTypes | Where-Object {
                $_.ResourceProvider -eq $fakeBoundParameters.ResourceProvider -and 
                $_.Resource -eq $fakeBoundParameters.Resource -and 
                $fakeBoundParameters.Child -like "$wordToComplete*"
            } | Select-Object -ExpandProperty Child -Unique | Sort-Object
        }
        else {
            $Children = (GetBicepTypes).Child | Where-Object { $_ -like "$wordToComplete*" } | Sort-Object -Unique -Descending
        }
        
        $list = [System.Collections.Generic.List[System.Management.Automation.CompletionResult]]::new()
        
        foreach ($Child in $Children) {
            $CompletionText = $Child
            $ListItemText   = $Child
            $ResultType     = [System.Management.Automation.CompletionResultType]::ParameterValue
            
            $ToolTip = '{0}/{1}/{2}' -f $fakeBoundParameters.ResourceProvider, $fakeBoundParameters.Resource, $Child
            $ToolTip = $ToolTip.TrimEnd('/')
  
            $obj = [System.Management.Automation.CompletionResult]::new($CompletionText, $ListItemText, $ResultType, $Tooltip)
            $list.add($obj)
        }

        return $list
        
    }
 }

 class BicepResourceApiVersionCompleter : System.Management.Automation.IArgumentCompleter{
    [System.Collections.Generic.IEnumerable[System.Management.Automation.CompletionResult]] CompleteArgument(
        [string] $CommandName,
        [string] $ParameterName,
        [string] $wordToComplete,
        [System.Management.Automation.Language.CommandAst] $CommandAst,
        [Collections.IDictionary] $fakeBoundParameters
    )
    {
        if ($fakeBoundParameters.ContainsKey('ResourceProvider') -and $fakeBoundParameters.ContainsKey('Resource')) {
            $ApiVersions = GetBicepTypes | Where-Object {
                $_.ResourceProvider -eq $fakeBoundParameters.ResourceProvider -and 
                $_.Resource -eq $fakeBoundParameters.Resource -and 
                $fakeBoundParameters.ApiVersion -like "$wordToComplete*"
            } | Select-Object -ExpandProperty ApiVersion -Unique | Sort-Object -Descending
        }
        elseif ($fakeBoundParameters.ContainsKey('ResourceProvider') -and $fakeBoundParameters.ContainsKey('Resource') -and $fakeBoundParameters.ContainsKey('Child')) {
            $ApiVersions = GetBicepTypes | Where-Object {
                $_.ResourceProvider -eq $fakeBoundParameters.ResourceProvider -and 
                $_.Resource -eq $fakeBoundParameters.Resource -and
                $_.Child -eq $fakeBoundParameters.Child -and 
                $fakeBoundParameters.ApiVersion -like "$wordToComplete*"
            } | Select-Object -ExpandProperty ApiVersion -Unique | Sort-Object -Descending
        }
        else {
            $ApiVersions = (GetBicepTypes).ApiVersion | Where-Object { $_ -like "$wordToComplete*" } | Sort-Object -Unique -Descending
        }
        
        $list = [System.Collections.Generic.List[System.Management.Automation.CompletionResult]]::new()
        
        foreach ($ApiVersion in $ApiVersions) {
            $CompletionText = $ApiVersion
            $ListItemText   = $ApiVersion
            $ResultType     = [System.Management.Automation.CompletionResultType]::ParameterValue
            
            $ToolTip = '{0}/{1}/{2}' -f $fakeBoundParameters.ResourceProvider, $fakeBoundParameters.Resource, $fakeBoundParameters.Child
            $ToolTip = $ToolTip.TrimEnd('/') + "@$ApiVersion"
            
            $obj = [System.Management.Automation.CompletionResult]::new($CompletionText, $ListItemText, $ResultType, $Tooltip)
            $list.add($obj)
        }

        return $list
        
    }
 }

 class BicepTypeCompleter : System.Management.Automation.IArgumentCompleter{
    [System.Collections.Generic.IEnumerable[System.Management.Automation.CompletionResult]] CompleteArgument(
        [string] $CommandName,
        [string] $ParameterName,
        [string] $wordToComplete,
        [System.Management.Automation.Language.CommandAst] $CommandAst,
        [Collections.IDictionary] $fakeBoundParameters
    )
    {
        $Types = (GetBicepTypes).FullName | Where-Object { $_ -like "$wordToComplete*" } | Sort-Object -Unique -Descending
        
        $list = [System.Collections.Generic.List[System.Management.Automation.CompletionResult]]::new()
        
        foreach ($Type in $Types) {
            $CompletionText = $Type
            $ListItemText   = $Type
            $ResultType     = [System.Management.Automation.CompletionResultType]::ParameterValue
            $ToolTip        = $Type
  
            $obj = [System.Management.Automation.CompletionResult]::new($CompletionText, $ListItemText, $ResultType, $Tooltip)
            $list.add($obj)
        }

        return $list
        
    }
 }
#endregion BicepTypesCompleters

#region BicepVersionsCompleters

class BicepVersionCompleter : System.Management.Automation.IArgumentCompleter{
    [System.Collections.Generic.IEnumerable[System.Management.Automation.CompletionResult]] CompleteArgument(
        [string] $CommandName,
        [string] $ParameterName,
        [string] $wordToComplete,
        [System.Management.Automation.Language.CommandAst] $CommandAst,
        [Collections.IDictionary] $fakeBoundParameters
    )
    {
        [array]$BicepVersions = (ListBicepVersions) | Where-Object { $_ -like "$wordToComplete*" }
        
        $list = [System.Collections.Generic.List[System.Management.Automation.CompletionResult]]::new()
        
        foreach ($BicepVersion in $BicepVersions) {
            $CompletionText = $BicepVersion
            $ListItemText   = $BicepVersion
            $ResultType     = [System.Management.Automation.CompletionResultType]::ParameterValue
            $ToolTip        = $BicepVersion
  
            $obj = [System.Management.Automation.CompletionResult]::new($CompletionText, $ListItemText, $ResultType, $Tooltip)
            $list.add($obj)
        }

        return $list
        
    }
 }

#endregion BicepVersionsCompleters

#region CompareBicepVersion

function CompareBicepVersion {
    $installedVersion = InstalledBicepVersion
    $latestVersion = ListBicepVersions -Latest

    if ($installedVersion -eq $latestVersion) {
        $true
    }
    else {
        $false
    }
}
#endregion CompareBicepVersion

#region ConvertToHashtable

function ConvertToHashtable {
    param(
        [Parameter(ValueFromPipeline)]
        [object]
        $InputObject,
        [switch]
        $Ordered
    )
    process {
 
        # If InputObject is string or valuetype, don't recurse, just return the value.
        if (
            $null -eq $InputObject -or 
            $InputObject.GetType().FullName -eq 'System.String' -or 
            $InputObject.GetType().IsValueType -or
            $InputObject -is [System.Collections.Specialized.OrderedDictionary] -or
            $InputObject -is [System.Collections.Hashtable]
        ) {
            return $InputObject
        }
 
        # Else, create a hashtable and loop over properties.
        if ($Ordered.IsPresent) {
            $HashTable = [ordered]@{}
        }
        else {
            $HashTable = @{}
        }
        foreach ($Prop in $InputObject.psobject.Properties) {
            if (
                $null -eq $Prop.Value -or 
                $Prop.TypeNameOfValue -eq 'System.String' -or 
                $Prop.Value.GetType().IsValueType -or
                $InputObject -is [System.Collections.Specialized.OrderedDictionary] -or
                $InputObject -is [System.Collections.Hashtable]
            ) {
                $HashTable.Add($Prop.Name, $Prop.Value)
            }
            else {
                if ($Prop.TypeNameOfValue -eq 'System.Object[]' -and (-not $Prop.Value)) {
                    $Value = @()
                }
                elseif ($Prop.TypeNameOfValue -eq 'System.Object[]' -and $Prop.Value) {
                    $Value = @($Prop.Value)
                }
                else {
                    $Value = $Prop.Value | ConvertToHashtable -Ordered:$Ordered
                }
                $HashTable.Add($Prop.Name, $Value)
            }
        }
        return $HashTable
    }
}
#endregion ConvertToHashtable

#region GenerateParameterFile

function GenerateParameterFile {
    [CmdletBinding(DefaultParameterSetName = 'FromFile',
        SupportsShouldProcess)]
    param (
        [Parameter(Mandatory,
            ParameterSetName = 'FromFile')]
        [object]$File,

        [Parameter(Mandatory,
            ParameterSetName = 'FromContent')]
        [ValidateNotNullOrEmpty()]
        [string]$Content,

        [Parameter(Mandatory,
            ParameterSetName = 'FromContent')]
        [ValidateNotNullOrEmpty()]
        [string]$DestinationPath,

        [Parameter(ParameterSetName = 'FromFile')]
        [Parameter(ParameterSetName = 'FromContent')]
        [string]$Parameters
    )

    if ($PSCmdlet.ParameterSetName -eq 'FromFile') {
        $fileName = $file.Name -replace ".bicep", ""
        $ARMTemplate = Get-Content "$($file.DirectoryName)\$filename.json" -Raw | ConvertFrom-Json
    }
    elseif ($PSCmdlet.ParameterSetName -eq 'FromContent') {
        $ARMTemplate = $Content | ConvertFrom-Json
    }
    
    $parameterBase = [ordered]@{
        '$schema'        = 'https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#'
        'contentVersion' = '1.0.0.0'
    }
    $parameterNames = $ARMTemplate.Parameters.psobject.Properties.Name
    if (-not $parameterNames) {
        Write-Host "No parameters declared in the specified bicep file."
        break
    }
    else {
        $parameterHash = [ordered]@{}
        foreach ($parameterName in $parameterNames) {
            $ParameterObject = $ARMTemplate.Parameters.$ParameterName
            if (($Parameters -eq "Required" -and $null -eq $ParameterObject.defaultValue) -or ($Parameters -eq "All")) {
                if ($null -eq $ParameterObject.defaultValue) {                               
                    if ($ParameterObject.type -eq 'Array') {
                        $defaultValue = @()
                    }
                    elseif ($ParameterObject.type -eq 'Object') {
                        $defaultValue = @{}
                    }
                    elseif ($ParameterObject.type -eq 'int') {
                        $defaultValue = 0
                    }
                    else {
                        $defaultValue = ""
                    }
                }
                elseif ($ParameterObject.defaultValue -like "*()*") {
                    $defaultValue = ""
                }
                else {
                    $defaultValue = $ParameterObject.defaultValue
                }
                $parameterHash[$parameterName] = @{                                
                    value = $defaultValue
                }
            }                       
        }
        $parameterBase['parameters'] = $parameterHash
        $ConvertedToJson = ConvertTo-Json -InputObject $parameterBase -Depth 100
    
        switch ($PSCmdlet.ParameterSetName) {
            'FromFile' {
                Out-File -InputObject $ConvertedToJson -FilePath "$($file.DirectoryName)\$filename.parameters.json" -WhatIf:$WhatIfPreference
            }
            'FromContent' {
                Out-File -InputObject $ConvertedToJson -FilePath $DestinationPath -WhatIf:$WhatIfPreference
            }
            Default {
                Write-Error "Unable to generate parameter file. Unknown parameter set: $($PSCmdlet.ParameterSetName)"
            }
        }
    }
}
#endregion GenerateParameterFile

#region GetBicepTypes

function GetBicepTypes {
    [CmdletBinding()]
    param (
        [ValidateNotNullOrEmpty()]
        [string]$Path
    )
    
    if (-not [string]::IsNullOrEmpty($Path)) {
        Write-Verbose "Importing Bicep Types"
        $types = Get-Content -Path $Path | ConvertFrom-Json -AsHashtable

        $allResourceProviders = [System.Collections.ArrayList]::new()
        
        foreach ($type in $types) {
            # Type looks like this: Microsoft.Aad/domainServicess@2017-01-01
            # We want to split here: ^ ^
            # Or like this: Microsoft.ApiManagement/service/certificates@2019-12-01
            # Then we split here: ^ ^ ^
            
            # First check if we have three parts before the @
            # In that case the last one should be the child
            if (($type -split '/' ).count -eq 3) {
                $child = ( ($type -split '@') -split '/' )[2]
            }  
            else {
                $child = $null
            }

            $ResourceProviders = [PSCustomObject]@{
                ResourceProvider = ( ($type -split '@') -split '/' )[0]
                Resource         = ( ($type -split '@') -split '/' )[1]
                Child            = $child
                ApiVersion       = ( $type -split '@' )[1]
                FullName         = $type
            }
            
            $null = $allResourceProviders.Add($ResourceProviders)
        }
        $Script:BicepResourceProviders = $allResourceProviders
    }

    Write-Output -InputObject $Script:BicepResourceProviders
}

#endregion GetBicepTypes

#region InstalledBicepVersion

function InstalledBicepVersion {   
    if (TestBicep) {
        $Version=((bicep --version) -split "\s+")[3]
        "$Version"
    } else {
        "Not installed"
    }  
}
#endregion InstalledBicepVersion

#region ListBicepVersions

function ListBicepVersions {
    [CmdletBinding()]
    param (
        [switch]$Latest

    )
    $BaseURL = 'https://api.github.com/repos/Azure/bicep/releases'
    
    if ($Latest) {
        try {
            $LatestVersion = Invoke-RestMethod -Uri ('{0}/latest' -f $BaseURL)
            $LatestVersion.tag_name -replace '[v]', ''
        }
        catch {
            Write-Error -Message "Could not get latest version from GitHub. $_" -Category ObjectNotFound
        }
    }
    else {
        try {
            $AvailableVersions = Invoke-RestMethod -Uri $BaseURL
            $AvailableVersions.tag_name -replace '[v]', ''   
        }
        catch {
            Write-Error -Message "Could not get versions from GitHub. $_" -Category ObjectNotFound
        }
    }
}
#endregion ListBicepVersions

#region TestBicep

function TestBicep {
    $bicep = (Get-Command bicep -ErrorAction SilentlyContinue)
    if ($bicep) {
        $true
    }
    else {
        $false
    }
}
#endregion TestBicep

#region TestModuleVersion

function TestModuleVersion {
        
    $BaseURL = 'https://api.github.com/repos/PSBicep/PSBicep/releases'
    try {        
        $LatestVersion = Invoke-RestMethod -Uri ('{0}/latest' -f $BaseURL) -TimeoutSec 1 -Verbose:$false
        $LatestBicepVersion = $LatestVersion.tag_name -replace '[v]', ''

        $InstalledModuleVersion = (Get-Module -Name Bicep).Version | Sort-Object -Descending | Select-Object -First 1
        
        if($LatestBicepVersion -as [version]) {
            if([version]$LatestBicepVersion -gt $InstalledModuleVersion) {
                Write-Host "A new version of the Bicep module ($LatestBicepVersion) is available. Update the module using 'Update-Module -Name Bicep'" -ForegroundColor 'DarkYellow'
            }
        }
        else {
            Write-Host "Failed to compare Bicep module version. Latest version is $LatestBicepVersion. Update the module using 'Update-Module -Name Bicep'" -ForegroundColor 'DarkYellow'
        }
    }
    catch {
        Write-Verbose -Message "Could not find a newer version of the module. $_"
    }
    
    $Script:ModuleVersionChecked = $true
    
}
#endregion TestModuleVersion

#region WriteBicepDiagnostic

function WriteBicepDiagnostic {
    [CmdletBinding()]
    param (
        [Bicep.Core.Diagnostics.Diagnostic]$Diagnostic,

        [Bicep.Core.Syntax.SyntaxTree]$SyntaxTree
    )
    Write-Warning 'WriteBicepDiagnostic does not work anymore, I''m sorry'
    return
    
    $FileUri = $SyntaxTree.FileUri
    $LocalPath = $FileUri.LocalPath
    $LineStarts = $SyntaxTree.LineStarts

    $Position = [Bicep.Core.Text.TextCoordinateConverter]::GetPosition($LineStarts, $Diagnostic.Span.Position)
    [int]$Line = $Position.Item1 + 1
    [int]$Character = $Position.Item2 + 1

    $Level = $Diagnostic.Level.ToString()
    $Code = $Diagnostic.Code
    $Message = $Diagnostic.Message
    $OutputString = "$LocalPath(${Line},$Character) : $Level ${Code}: $Message"

    switch ($Diagnostic.Level) {
        'Info' {
            $Params = @{
                MessageData = [System.Management.Automation.HostInformationMessage]@{
                    Message         = $OutputString
                    ForegroundColor = $Host.PrivateData.VerboseForegroundColor
                    BackgroundColor = $Host.PrivateData.VerboseBackgroundColor
                }
                Tag         = 'Information'
            }
        }
        'Warning' {
            $Params = @{
                MessageData = [System.Management.Automation.HostInformationMessage]@{
                    Message         = $OutputString
                    ForegroundColor = $Host.PrivateData.WarningForegroundColor
                    BackgroundColor = $Host.PrivateData.WarningBackgroundColor
                }
                Tag         = 'Warning'
            }
        }
        'Error' {
            $Params = @{
                MessageData = [System.Management.Automation.HostInformationMessage]@{
                    Message         = $OutputString
                    ForegroundColor = $Host.PrivateData.ErrorForegroundColor
                    BackgroundColor = $Host.PrivateData.ErrorBackgroundColor
                }
                Tag         = 'Error'
            }
        }
        'Off' {
            $Params = @{
                MessageData = [System.Management.Automation.HostInformationMessage]@{
                    Message         = $OutputString
                    ForegroundColor = $Host.PrivateData.VerboseForegroundColor
                    BackgroundColor = $Host.PrivateData.VerboseBackgroundColor
                }
                Tag         = 'Off'
            }
        }
        default {
            Write-Warning "Unhandled diagnostic level: $_"
        }
    }

    return $Params
}
#endregion WriteBicepDiagnostic

#region WriteBicepNetDiagnostic

function WriteBicepNetDiagnostic {
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline)]
        [BicepDiagnosticEntry[]]
        $Diagnostic
    )
    
    process {
        foreach($DiagnosticEntry in $Diagnostic) {
            $LocalPath = $DiagnosticEntry.LocalPath

            [int]$Line = $DiagnosticEntry.Position[0] + 1
            [int]$Character = $DiagnosticEntry.Position[1] + 1

            $Level = $DiagnosticEntry.Level.ToString()
            $Code = $DiagnosticEntry.Code
            $Message = $DiagnosticEntry.Message
            $OutputString = "$LocalPath(${Line},$Character) : $Level ${Code}: $Message"

            switch ($Level) {
                'Info' {
                    $Params = @{
                        MessageData = [System.Management.Automation.HostInformationMessage]@{
                            Message         = $OutputString
                            ForegroundColor = $Host.PrivateData.VerboseForegroundColor
                            BackgroundColor = $Host.PrivateData.VerboseBackgroundColor
                        }
                        Tag         = 'Information'
                    }
                }
                'Warning' {
                    $Params = @{
                        MessageData = [System.Management.Automation.HostInformationMessage]@{
                            Message         = $OutputString
                            ForegroundColor = $Host.PrivateData.WarningForegroundColor
                            BackgroundColor = $Host.PrivateData.WarningBackgroundColor
                        }
                        Tag         = 'Warning'
                    }
                }
                'Error' {
                    $Params = @{
                        MessageData = [System.Management.Automation.HostInformationMessage]@{
                            Message         = $OutputString
                            ForegroundColor = $Host.PrivateData.ErrorForegroundColor
                            BackgroundColor = $Host.PrivateData.ErrorBackgroundColor
                        }
                        Tag         = 'Error'
                    }
                }
                'Off' {
                    $Params = @{
                        MessageData = [System.Management.Automation.HostInformationMessage]@{
                            Message         = $OutputString
                            ForegroundColor = $Host.PrivateData.VerboseForegroundColor
                            BackgroundColor = $Host.PrivateData.VerboseBackgroundColor
                        }
                        Tag         = 'Off'
                    }
                }
                default {
                    Write-Warning "Unhandled diagnostic level: $_"
                }
            }
        
            Write-Information @Params
        }
    }
}
#endregion WriteBicepNetDiagnostic

#region WriteErrorStream

function WriteErrorStream {
    [CmdletBinding()]
    param (
        [string]$String
    )
    
    if ($Host.Name -eq 'ConsoleHost') {
        [Console]::Error.WriteLine($String)
    }
    else {
        $Host.UI.WriteErrorLine($String)
    }
}
#endregion WriteErrorStream

#region Build-Bicep

function Build-Bicep {
    [CmdletBinding(DefaultParameterSetName = 'Default',
        SupportsShouldProcess)]
    [Alias('Invoke-BicepBuild')]
    param (
        [Parameter(ParameterSetName = 'Default', Position = 1)]
        [Parameter(ParameterSetName = 'AsString', Position = 1)]
        [Parameter(ParameterSetName = 'AsHashtable', Position = 1)]
        [Parameter(ParameterSetName = 'OutputPath', Position = 1)]
        [string]$Path = $pwd.path,

        [Parameter(ParameterSetName = 'Default', Position = 2)]
        [ValidateNotNullOrEmpty()]
        [string]$OutputDirectory,

        [Parameter(ParameterSetName = 'OutputPath', Position = 2)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript(
            {
                (Split-path -path $_ -leaf) -match "\.json$"
            }
            , ErrorMessage = 'OutputPath needs to be a .JSON-file, e.g. "C:\Output\template.json"')]
        [string]$OutputPath,

        [Parameter(ParameterSetName = 'Default')]
        [Parameter(ParameterSetName = 'AsString')]
        [Parameter(ParameterSetName = 'AsHashtable')]
        [Parameter(ParameterSetName = 'OutputPath')]
        [string[]]$ExcludeFile,

        [Parameter(ParameterSetName = 'Default')]
        [Parameter(ParameterSetName = 'OutputPath')]
        [switch]$GenerateAllParametersFile,

        [Parameter(ParameterSetName = 'Default')]
        [Parameter(ParameterSetName = 'OutputPath')]
        [switch]$GenerateRequiredParametersFile,

        [Parameter(ParameterSetName = 'AsString')]
        [switch]$AsString,

        [Parameter(ParameterSetName = 'AsHashtable')]
        [switch]$AsHashtable,

        [Parameter(ParameterSetName = 'Default')]
        [Parameter(ParameterSetName = 'AsString')]
        [Parameter(ParameterSetName = 'AsHashtable')]
        [Parameter(ParameterSetName = 'OutputPath')]
        [switch]$NoRestore
    )

    begin {
        if (-not $Script:ModuleVersionChecked) {
            TestModuleVersion
        }
        if ($PSBoundParameters.ContainsKey('OutputDirectory') -and (-not (Test-Path $OutputDirectory))) {
            $null = New-Item $OutputDirectory -Force -ItemType Directory -WhatIf:$WhatIfPreference
        }
        if ($PSBoundParameters.ContainsKey('OutputPath') -and (-not (Test-Path $OutputPath))) {
            $null = New-Item (Split-Path -Path $OutputPath) -Force -ItemType Directory -WhatIf:$WhatIfPreference
        }
        if ($PSBoundParameters.ContainsKey('OutputPath') -and ((Split-path -path $Path -leaf) -notmatch "\.bicep$")) { 
            Write-Error 'If -Path and -OutputPath parameters are used, only one .bicep file can be used as input to -Path. E.g. -Path "C:\Output\template.bicep" -OutputPath "C:\Output\newtemplate.json".'
            Break
        }
        if ($VerbosePreference -eq [System.Management.Automation.ActionPreference]::Continue) {
            $FullVersion = Get-BicepNetVersion -Verbose:$false
            Write-Verbose -Message "Using Bicep version: $FullVersion"
        }
    }

    process {
        $files = Get-Childitem -Path $Path *.bicep -File
        if ($files) {
            foreach ($file in $files) {
                if ($file.Name -notin $ExcludeFile) {
                    if ($NoRestore.IsPresent) {
                        $BuildResult = Build-BicepNetFile -Path $file.FullName -NoRestore
                    } else {
                        $BuildResult = Build-BicepNetFile -Path $file.FullName
                    }

                    $ARMTemplate = $BuildResult[0]

                    if (-not [string]::IsNullOrWhiteSpace($ARMTemplate)) {
                        $BicepModuleVersion = Get-Module -Name Bicep | Sort-Object -Descending | Select-Object -First 1
                        $ARMTemplateObject = ConvertFrom-Json -InputObject $ARMTemplate
                        if (-not [string]::IsNullOrWhiteSpace($BicepModuleVersion.PrivateData.Values.Prerelease)) {
                            $ARMTemplateObject.metadata._generator.name += " (Bicep PowerShell $($BicepModuleVersion.Version)-$($BicepModuleVersion.PrivateData.Values.Prerelease))"
                        }
                        else {
                            $ARMTemplateObject.metadata._generator.name += " (Bicep PowerShell $($BicepModuleVersion.Version))"
                        }
                        $ARMTemplate = ConvertTo-Json -InputObject $ARMTemplateObject -Depth 100
                        if ($AsString.IsPresent) {
                            Write-Output $ARMTemplate
                        }
                        elseif ($AsHashtable.IsPresent) {
                            $ARMTemplateObject | ConvertToHashtable -Ordered
                        }
                        else {        
                            if ($PSBoundParameters.ContainsKey('OutputPath')) {
                                $OutputFilePath = $OutputPath
                                $ParameterFilePath = Join-Path -Path (Split-Path -Path $OutputPath) -ChildPath ('{0}.parameters.json' -f (Split-Path -Path $OutputPath -Leaf).Split(".")[0])
                            }
                            elseif ($PSBoundParameters.ContainsKey('OutputDirectory')) {
                                $OutputFilePath = Join-Path -Path $OutputDirectory -ChildPath ('{0}.json' -f $file.BaseName)
                                $ParameterFilePath = Join-Path -Path $OutputDirectory -ChildPath ('{0}.parameters.json' -f $file.BaseName)
                            }
                            else {
                                $OutputFilePath = $file.FullName -replace '\.bicep', '.json'
                                $ParameterFilePath = $file.FullName -replace '\.bicep', '.parameters.json'
                            }
                            $null = Out-File -Path $OutputFilePath -InputObject $ARMTemplate -Encoding utf8 -WhatIf:$WhatIfPreference
                            if ($GenerateRequiredParametersFile.IsPresent -and $GenerateAllParametersFile.IsPresent) {
                                $parameterType = 'All'                                    
                                Write-Warning "Both -GenerateAllParametersFile and -GenerateRequiredParametersFile is present. A parameter file with all parameters will be generated."
                            }
                            elseif ($GenerateRequiredParametersFile.IsPresent) {
                                $parameterType = 'Required'
                            }
                            elseif ($GenerateAllParametersFile.IsPresent) {
                                $parameterType = 'All'
                            }

                            if ($GenerateAllParametersFile.IsPresent -or $GenerateRequiredParametersFile.IsPresent) {                                
                                GenerateParameterFile -Content $ARMTemplate -DestinationPath $ParameterFilePath -Parameters $parameterType -WhatIf:$WhatIfPreference
                            }
                        }
                    }
                }
            }
        }
        else {
            Write-Host "No bicep files located in path $Path"
        }
    }
}
#endregion Build-Bicep

#region Clear-BicepModuleCache

function Clear-BicepModuleCache {
    [CmdletBinding(DefaultParameterSetName = 'Oci')]
    param (
        
        [Parameter(ParameterSetName = 'Oci', Position = 1)]
        [switch]$Oci,

        [Parameter(ParameterSetName = 'Oci', Position = 2)]
        [ValidateNotNullOrEmpty()]
        [string]$Registry,

        [Parameter(ParameterSetName = 'Oci', Position = 3)]
        [ValidateNotNullOrEmpty()]
        [string]$Repository,

        [Parameter(ParameterSetName = 'TemplateSpecs', Position = 1)]
        [switch]$TemplateSpecs,

        [Parameter(ParameterSetName = 'TemplateSpecs', Position = 2)]
        [ValidateNotNullOrEmpty()]
        [string]$SubscriptionId,

        [Parameter(ParameterSetName = 'TemplateSpecs', Position = 3)]
        [ValidateNotNullOrEmpty()]
        [string]$ResourceGroup,

        [Parameter(ParameterSetName = 'TemplateSpecs', Position = 4)]
        [ValidateNotNullOrEmpty()]
        [string]$Spec,

        [Parameter(ParameterSetName = 'Oci', Position = 4)]
        [Parameter(ParameterSetName = 'TemplateSpecs', Position = 5)]
        [ValidateNotNullOrEmpty()]
        [string]$Version,

        [Parameter(ParameterSetName = 'All', Position = 1)]
        [switch]$All
     
    )
    begin {
        # Check if a newer version of the module is published
        if (-not $Script:ModuleVersionChecked) {
            TestModuleVersion
        }       
    }

    process {
        
        if ($Oci -or $All) {            
            $OciPath = Get-BicepNetCachePath -Oci 
            $RepositoryPath = $Repository -replace '\\', '$'

            if (($Registry) -and ($Repository) -and ($Version)) {
                Remove-Item -Recurse -Path "$OciPath/$Registry/$RepositoryPath/$Version`$"
                Write-Verbose "Cleared version [$Version] of [$Repository] in [$Registry] from local module cache"
            }
            elseif (($Registry) -and ($Repository)) {
                Remove-Item -Recurse -Path "$OciPath/$Registry/$RepositoryPath"
                Write-Verbose "Cleared [$Repository] in [$Registry] from local module cache"
            }
            elseif ($Registry) {
                Remove-Item -Recurse -Path "$OciPath/$Registry"
                Write-Verbose "Cleared [$Registry] from local module cache" 
            }
            else {
                if (Test-Path -Path $OciPath) {
                    Remove-Item -Recurse -Path $OciPath
                    Write-Verbose "Cleared Oci local module cache" 
                }
                else {
                    Write-Verbose "No Oci local module cache found" 
                }
            }            
        }
        
        if ($TemplateSpecs -or $All) {            
            $TSPath = Get-BicepNetCachePath -TemplateSpecs

            if (($SubscriptionId) -and ($ResourceGroup) -and ($Spec) -and ($Version)) {
                Remove-Item -Recurse -Path "$TSPath/$SubscriptionId/$ResourceGroup/$Spec/$Version"
                Write-Verbose "Cleared version [$Version] of [$Spec] in [$ResourceGroup] in [$SubscriptionId] from local module cache"
            }
            elseif (($SubscriptionId) -and ($ResourceGroup) -and ($Spec)) {
                Remove-Item -Recurse -Path "$TSPath/$SubscriptionId/$ResourceGroup/$Spec"
                Write-Verbose "Cleared [$Spec] in [$ResourceGroup] in [$SubscriptionId] from local module cache"
            }
            elseif (($SubscriptionId) -and ($ResourceGroup)) {
                Remove-Item -Recurse -Path "$TSPath/$SubscriptionId/$ResourceGroup"
                Write-Verbose "Cleared [$ResourceGroup] in [$SubscriptionId] from local module cache"
            }
            elseif ($SubscriptionId) {
                Remove-Item -Recurse -Path "$TSPath/$SubscriptionId"
                Write-Verbose "Cleared [$SubscriptionId] from local module cache" 
            }
            else {
                if (Test-Path -Path $TSPath) {
                    Remove-Item -Recurse -Path $TSPath
                    Write-Verbose "Cleared Template Spec local module cache" 
                }
                else {
                    Write-Verbose "No Template Spec local module cache found" 
                }
            }            
        }            
    }
}
#endregion Clear-BicepModuleCache

#region Convert-BicepParamsToDecoratorStyle

function Convert-BicepParamsToDecoratorStyle {
    [CmdletBinding()]
    param(
        [string]$Path,
        [switch]$ToClipboard
    )

    Write-Error "The Convert-BicepParamsToDecoratorStyle has been decommissioned starting version 1.5.0 of the module."  
    Write-Error "Parameter modifiers are no longer supported in Bicep v0.4 which is used by this version of the module."
    Write-Error "To be able to convert parameters to decorator style use version 1.4.7 of the Bicep module which is using Bicep v0.3.539 assemblies."
}
#endregion Convert-BicepParamsToDecoratorStyle

#region Convert-JsonToBicep

function Convert-JsonToBicep {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,
            ValueFromPipeline = $true,
            ParameterSetName = 'String')]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( {
                try { $_ | Convertfrom-Json }
                catch { $false }
            },
            ErrorMessage = 'The string is not a valid json')]
        [string]$String,
        [Parameter(ParameterSetName = 'Path')]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( {
                try { Get-Content -Path $_ | Convertfrom-Json }
                catch { $false }
            },
            ErrorMessage = 'The file does not contain a valid json')]
        [string]$Path,
        [switch]$ToClipboard
    )

    begin {
        if (-not $Script:ModuleVersionChecked) {
            TestModuleVersion
        }
        $tempPath = [System.Io.Path]::GetTempPath()
    }

    process {

        if($String) {
            $inputObject = $String | ConvertFrom-Json
        }
        else {
            $inputObject = Get-Content -Path $Path | ConvertFrom-Json
        }

        if ((-not $IsWindows) -and $ToClipboard.IsPresent) {
            Write-Error -Message "The -ToClipboard switch is only supported on Windows systems."
            break
        }

        $hashTable = ConvertToHashtable -InputObject $inputObject -Ordered
        $variables = [ordered]@{}
        $templateBase = [ordered]@{
            '$schema'        = 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#'
            'contentVersion' = '1.0.0.0'
        }
        $variables['temp'] = $hashTable.SyncRoot
        $templateBase['variables'] = $variables
        $tempTemplate = ConvertTo-Json -InputObject $templateBase -Depth 100
        Out-File -InputObject $tempTemplate -FilePath "$tempPath\tempfile.json"
        $file = Get-ChildItem -Path "$tempPath\tempfile.json"

        if ($file) {
            $BicepObject = ConvertTo-BicepNetFile -Path $file.FullName
            foreach ($BicepFile in $BicepObject.Keys) {
                $bicepData = $BicepObject[$BicepFile]
            }
            $bicepOutput = $bicepData.Replace("var temp = ", "")
            if ($ToClipboard.IsPresent) {                
                Set-Clipboard -Value $bicepOutput
                Write-Host "Bicep object saved to clipboard"
            }
            else {
                Write-Output $bicepOutput
            }
        }
        Remove-Item $file
    }
}
#endregion Convert-JsonToBicep

#region ConvertTo-Bicep

function ConvertTo-Bicep {
    [CmdletBinding()]
    param(
        [string]$Path = $pwd.path,
        
        [string]$OutputDirectory,

        [switch]$AsString,

        [switch]$Force
    )

    begin {
        if (-not $Script:ModuleVersionChecked) {
            TestModuleVersion
        }
        Write-Warning -Message 'Decompilation is a best-effort process, as there is no guaranteed mapping from ARM JSON to Bicep.
You may need to fix warnings and errors in the generated bicep file(s), or decompilation may fail entirely if an accurate conversion is not possible.
If you would like to report any issues or inaccurate conversions, please see https://github.com/Azure/bicep/issues.'

        
        if ($PSBoundParameters.ContainsKey('OutputDirectory') -and (-not (Test-Path $OutputDirectory))) {
            $null = New-Item $OutputDirectory -Force -ItemType Directory
        }
        
    }

    process {
        $files = Get-Childitem -Path $Path -Filter '*.json' -File | Select-String -Pattern "schema.management.azure.com/schemas/.*deploymentTemplate.json#" | Select-Object -ExpandProperty 'Path' 
        if ($files) {
            foreach ($File in $files) {
                $BicepObject = ConvertTo-BicepNetFile -Path $File
                foreach ($BicepFile in $BicepObject.Keys) {
                    if ($AsString.IsPresent) {
                        Write-Output $BicepObject[$BicepFile]
                        $TempFolder = New-Item -Path ([system.io.path]::GetTempPath()) -Name (New-Guid).Guid -ItemType 'Directory'
                        $OutputDirectory = $TempFolder.FullName
                    }

                    if (-not [string]::IsNullOrEmpty($OutputDirectory)) {
                        $FileName = Split-Path -Path $BicepFile -Leaf
                        $FilePath = Join-Path -Path $OutputDirectory -ChildPath $FileName
                    }
                    else {
                        $FilePath = $BicepFile
                    }
                    
                    if ((Test-Path $FilePath) -and (-not $Force)) {
                        Write-Error -Message "$FilePath Already exists. Use -Force to overwrite." -Category ResourceExists -TargetObject $FilePath
                        $VerifyBicepBuild = $false
                    }
                    else {
                        $null = Out-File -InputObject $BicepObject[$BicepFile] -FilePath $FilePath -Encoding utf8
                        $VerifyBicepBuild = $true
                    }
                }
                
                if ($VerifyBicepBuild) {
                    $null = Build-Bicep -Path $FilePath -AsString
                }

                if($null -ne $TempFolder) {
                    Remove-Item -Path $TempFolder -Recurse -Force
                }
            }
        }
        else {
            Write-Host "No ARM template files located in path $Path"
        }
    }
}
#endregion ConvertTo-Bicep

#region Find-BicepModule

function Find-BicepModule {
    [CmdletBinding()]
    param (
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( { Test-Path $_ })]
        [string]$Path,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Registry,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [switch]$Cache

    )
    begin {
        # Check if a newer version of the module is published
        if (-not $Script:ModuleVersionChecked) {
            TestModuleVersion
        }       
    }

    process {        
        # Find modules used in local bicep file
        if ($Path) {
            $BicepFile = Get-Childitem -Path $Path -File
        
            try {
                $validBicep = Test-BicepFile -Path $BicepFile.FullName -IgnoreDiagnosticOutput -AcceptDiagnosticLevel Warning
                if (-not ($validBicep)) {
                    throw "The provided bicep is not valid. Make sure that your bicep file builds successfully before publishing."
                }
                else {
                    Write-Verbose "[$($BicepFile.Name)] is valid"
                    Find-BicepNetModule -Path $Path
                    Write-Verbose -Message "Finding modules used in [$($BicepFile.Name)]"
                }
            }
            catch {
                Throw $_  
            }
        }
        
        # Find modules in ACR
        if ($Registry) {
            try {
                Find-BicepNetModule -Registry $Registry
                Write-Verbose -Message "Finding all modules stored in: [$Registry]"
            }
            catch {
                Throw $_
            }
        }

        # Find modules in the local cache
        if ($Cache) {
            # Find module
            try {
                Find-BicepNetModule -Cache
                Write-Verbose -Message "Finding modules in the local module cache"
            }
            catch {
                
            }
        }
    }
}
#endregion Find-BicepModule

#region Get-BicepApiReference

function Get-BicepApiReference {
    [CmdletBinding(DefaultParameterSetName = 'TypeString')]
    param(
        [Parameter(Mandatory, 
            ParameterSetName = 'ResourceProvider')]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( { (GetBicepTypes).ResourceProvider -contains $_ }, 
            ErrorMessage = "ResourceProvider '{0}' was not found.")]
        [ArgumentCompleter([BicepResourceProviderCompleter])]
        [string]$ResourceProvider,

        [Parameter(Mandatory, 
            ParameterSetName = 'ResourceProvider')]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( { (GetBicepTypes).Resource -contains $_ }, 
            ErrorMessage = "Resource '{0}' was not found.")]
        [ArgumentCompleter([BicepResourceCompleter])]
        [string]$Resource,
        
        [Parameter(ParameterSetName = 'ResourceProvider')]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( { (GetBicepTypes).Child -contains $_ }, 
            ErrorMessage = "Child '{0}' was not found.")]
        [ArgumentCompleter([BicepResourceChildCompleter])]
        [string]$Child,

        [Parameter(ParameterSetName = 'ResourceProvider')]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( { (GetBicepTypes).ApiVersion -contains $_ }, 
            ErrorMessage = "ApiVersion '{0}' was not found.")]
        [ArgumentCompleter([BicepResourceApiVersionCompleter])]
        [string]$ApiVersion,

        [Parameter(ParameterSetName = 'TypeString',
            Position = 0)]
        [ValidateScript( { $_ -like '*/*' -and $_ -like '*@*' },
            ErrorMessage = "Type must contain '/' and '@'.")]
        [ArgumentCompleter([BicepTypeCompleter])]
        [string]$Type,

        [Parameter(ParameterSetName = 'TypeString')]
        [switch]$Latest,
        
        [Parameter(ParameterSetName = 'ResourceProvider')]
        [Parameter(ParameterSetName = 'TypeString')]
        [Alias('Please')]
        [switch]$Force
    )
    begin {
        if (-not $Script:ModuleVersionChecked) {
            TestModuleVersion
        }
    }
    
    process {
        $baseUrl = "https://docs.microsoft.com/en-us/azure/templates"
        $suffix = '?tabs=bicep'

        switch ($PSCmdlet.ParameterSetName) {
            
            'ResourceProvider' {
                $url = "$BaseUrl/$ResourceProvider" 
                
                # if ApiVersion is provided, we use that. Otherwise we skip version and go for latest
                if ($PSBoundParameters.ContainsKey('ApiVersion')) {
                    $url += "/$ApiVersion"
                }

                $url += "/$Resource"

                # Child is optional, so we only add it if provided
                if ($PSBoundParameters.ContainsKey('Child')) {
                    $url += "/$Child"
                }

                $url += $suffix
            }
            'TypeString' {
                if ($PSBoundParameters.ContainsKey('Type')) {
                    # Type looks like this: Microsoft.Aad/domainServicess@2017-01-01
                    # Then we split here: ^ ^
                    # Or it looks like this: Microsoft.ApiManagement/service/certificates@2019-12-01
                    # Then we split here: ^ ^ ^
                    # Lets not use regex. regex kills kittens

                    # First check if we have three parts before the @
                    # In that case the last one should be the child
                    if (($type -split '/' ).count -eq 3) {
                        $TypeChild = ( ($type -split '@') -split '/' )[2]
                    }  
                    else {
                        $TypeChild = $null
                    }

                    $TypeResourceProvider = ( ($type -split '@') -split '/' )[0]
                    $TypeResource = ( ($type -split '@') -split '/' )[1]
                    $TypeApiVersion = ( $type -split '@' )[1]
                
                    if ([string]::IsNullOrEmpty($TypeChild) -and ($Latest.IsPresent)) {
                        $url = "$BaseUrl/$TypeResourceProvider/$TypeResource"
                    }
                    elseif ([string]::IsNullOrEmpty($TypeChild)) {
                        $url = "$BaseUrl/$TypeResourceProvider/$TypeApiVersion/$TypeResource"
                    }
                    elseif ($Latest.IsPresent) {
                        $url = "$BaseUrl/$TypeResourceProvider/$TypeResource/$TypeChild"
                    }
                    else {
                        $url = "$BaseUrl/$TypeResourceProvider/$TypeApiVersion/$TypeResource/$TypeChild"
                    }

                    $url += $suffix
                }
                else {
                    # If Type is not provided, open the template start page
                    $url = $BaseUrl
                }
            }
        }
        
        # Check if there is any valid page on the generated Url
        # We don't want to send users to broken urls.
        try {
            $null = Invoke-WebRequest -Uri $url -ErrorAction Stop
            $DocsFound = $true
        }
        catch {
            $DocsFound = $false
        }

        # Now we know if its working or not. Open page or provide error message.
        if ($DocsFound -or $Force.IsPresent) {
            Start-Process $url
        }
        else {
            Write-Error "No documentation found. This usually means that no documentation has been written. Use the -Latest parameter to open the latest available API Version. Or if you would like to try anyway, use the -Force parameter. Url: $url"      
        }
    }
}
#endregion Get-BicepApiReference

#region Get-BicepVersion

function Get-BicepVersion {
    param (
        [switch]$All

    )
    if (-not $Script:ModuleVersionChecked) {
        TestModuleVersion
    }
    if ($All.IsPresent) {
        ListBicepVersions
    }
    else {
        $installedVersion = InstalledBicepVersion
        $latestVersion = ListBicepVersions -Latest
    
        [pscustomobject]@{
            InstalledVersion = $installedVersion
            LatestVersion    = $latestVersion
        } 
    }
}
#endregion Get-BicepVersion

#region Install-BicepCLI

function Install-BicepCLI {
    [CmdLetBinding()]
    param(
        [ValidateScript( { (ListBicepVersions).Contains($_) },
            ErrorMessage = "Bicep Version '{0}' was not found.")]
        [ArgumentCompleter([BicepVersionCompleter])]
        [string]$Version,
        [switch]$Force
    )

    if (-not $Script:ModuleVersionChecked) {
        TestModuleVersion
    }

    $BicepInstalled = TestBicep

    if (-not $IsWindows) {
        Write-Error -Message "This cmdlet is only supported for Windows systems. `
To install Bicep on your system see instructions on https://github.com/Azure/bicep"

        Write-Host "`nList the available module cmdlets by running 'Get-Help -Name Bicep'`n"
        break
    }

    $BaseURL = 'https://api.github.com/repos/Azure/bicep/releases'
    if ($PSBoundParameters.ContainsKey('Version')) {
        $BicepRelease = ('{0}/tags/v{1}' -f $BaseURL, $Version)
    }
    else {
        $BicepRelease = ('{0}/latest' -f $BaseURL)
    }

    if ($Force.IsPresent -and $BicepInstalled -eq $true) {
        Write-Warning 'You are running multiple installations of Bicep CLI. You can correct this by running Update-BicepCLI.'
    }

    if ($Force.IsPresent -or $BicepInstalled -eq $false) {
        # Fetch the latest Bicep CLI installer
        $DownloadFileName = 'bicep-setup-win-x64.exe'
        $TargetFileName = Join-Path -Path $env:TEMP -ChildPath $DownloadFileName

        # Fetch data about latest Bicep release from Github API
        $GetBicepRelease = Invoke-RestMethod -Uri $BicepRelease
        $RequestedGithubAsset = $GetBicepRelease.assets | Where-Object -Property Name -eq $DownloadFileName
        #Download and show progress.
        (New-Object Net.WebClient).DownloadFileAsync($RequestedGithubAsset.browser_download_url, $TargetFileName)
        Write-Verbose "Downloading $($RequestedGithubAsset.browser_download_url) to location $TargetFileName"
        do {
            $PercentComplete = [math]::Round((Get-Item $TargetFileName).Length / $RequestedGithubAsset.size * 100)
            Write-Progress -Activity 'Downloading Bicep' -PercentComplete $PercentComplete
            start-sleep 1
        } while ((Get-Item $TargetFileName).Length -lt $RequestedGithubAsset.size)

        # Wait for a bit to make sure we don't run into file locks
        Start-Sleep -Seconds 2

        # Run the installer in silent mode
        Write-Verbose "Running installer $TargetFileName /VERYSILENT"
        Start-Process $TargetFileName -ArgumentList '/VERYSILENT' -Wait -ErrorAction Stop
        $i = 0
        do {
            $i++
            Write-Progress -Activity 'Installing Bicep CLI' -CurrentOperation "$i - Running $DownloadFileName"
            Start-Sleep -Seconds 1
        } while (Get-Process $DownloadFileName.Replace('.exe', '') -ErrorAction SilentlyContinue)

        # Since bicep installer updates the $env:PATH we reload it in order to verify installation.
        $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")

        # Verify you can now access the 'bicep' command.
        if (TestBicep) {
            $bicep = InstalledBicepVersion
            Write-Host "Bicep version $bicep successfully installed!"
        }
        else {
            Write-Error "Error installing Bicep CLI"
        }

        # Remove the downloaded installer.
        Remove-Item $TargetFileName -ErrorAction SilentlyContinue -Force
    }
    else {
        $versionCheck = CompareBicepVersion
        if ($versionCheck) {
            Write-Host "The latest Bicep CLI Version is already installed."
        }
        else {
            Write-Host "Bicep CLI is already installed, but there is a newer release available. Use Update-BicepCLI or Install-BicepCLI -Force to updated to the latest release"
        }
    }
}
#endregion Install-BicepCLI

#region New-BicepParameterFile

function New-BicepParameterFile {
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory, Position = 1)]
        [string]$Path,

        [Parameter(Position = 2)]
        [ValidateNotNullOrEmpty()]
        [ValidateSet("All", "Required")]
        [string]$Parameters,        

        [Parameter(Position = 3)]
        [ValidateNotNullOrEmpty()]
        [string]$OutputDirectory
    )

    begin {
        if (-not $Script:ModuleVersionChecked) {
            TestModuleVersion
        }
        if ($PSBoundParameters.ContainsKey('OutputDirectory') -and (-not (Test-Path $OutputDirectory))) {
            $null = New-Item $OutputDirectory -Force -ItemType Directory -WhatIf:$WhatIfPreference
        }

        if ($VerbosePreference -eq [System.Management.Automation.ActionPreference]::Continue) {
            $FullVersion = Get-BicepNetVersion -Verbose:$false
            Write-Verbose -Message "Using Bicep version: $FullVersion"
        }
        
    }

    process {
        $File = Get-Item -Path $Path
        $validateBicepFile = Test-BicepFile -Path $File.FullName -AcceptDiagnosticLevel Warning -IgnoreDiagnosticOutput
        if (-not $validateBicepFile) {
            Write-Error -Message "$($File.FullName) have build errors, make sure that the Bicep template builds successfully and try again."
            Write-Host "`nYou can use either 'Test-BicepFile' or 'Build-Bicep' to verify that the template builds successfully.`n"
            break
        }
        if ($File) {
            $BuildResult = Build-BicepNetFile -Path $file.FullName
            $ARMTemplate = $BuildResult.Template[0]

            if($PSBoundParameters.ContainsKey('OutputDirectory')) {
                $OutputFilePath = Join-Path -Path $OutputDirectory -ChildPath ('{0}.parameters.json' -f $File.BaseName)
            }
            else {
                $OutputFilePath = $File.FullName -replace '\.bicep','.parameters.json'
            }
            if (-not $PSBoundParameters.ContainsKey('Parameters')){
                $Parameters='Required'
            }
            GenerateParameterFile -Content $ARMTemplate -Parameters $Parameters -DestinationPath $OutputFilePath -WhatIf:$WhatIfPreference
        }
        else {
            Write-Error "No bicep file named $Path was found!"
        }
    }
}
#endregion New-BicepParameterFile

#region Publish-Bicep

function Publish-Bicep {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, Position = 1)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({Test-Path $_})]
        [string]$Path,

        [Parameter(Mandatory, Position = 2)]
        [ValidateNotNullOrEmpty()]
        [ValidatePattern('^(?<Prefix>[bBrR]{2})(?<ACROrAlias>(:[\w\-_]+\.azurecr.io|\/[\w\-\._]+:))(?<path>[\w\/\-\._]+)(?<tag>:[\w\/\-\._]+)$', ErrorMessage = 'Target does not match pattern for registry. Specify a path to a registry using "br:", or "br/" if using an alias.')]
        [string]$Target          
    )
    begin {
        # Check if a newer version of the module is published
        if (-not $Script:ModuleVersionChecked) {
            TestModuleVersion
        }       
    }

    process {
        $BicepFile = Get-Childitem -Path $Path -File
            
        try {
            $validBicep = Test-BicepFile -Path $BicepFile.FullName -IgnoreDiagnosticOutput -AcceptDiagnosticLevel Warning
            if (-not ($validBicep)) {
                throw "The provided bicep is not valid. Make sure that your bicep file builds successfully before publishing."
            }
            else {
                Write-Verbose "[$($BicepFile.Name)] is valid"
            }
        }
        catch {
            Throw $_  
        }

        # Publish module
        try {
            Publish-BicepNetFile -Path $BicepFile.FullName -Target $Target -ErrorAction Stop
            Write-Verbose -Message "[$($BicepFile.Name)] published to: [$Target]"
        }
        catch {
            Throw $_
        }
    }
}
#endregion Publish-Bicep

#region Restore-Bicep

function Restore-Bicep {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, Position = 1)]
        [ValidateNotNullOrEmpty()]
        [string]$Path         
    )

    begin {
        # Check if a newer version of the module is published
        if (-not $Script:ModuleVersionChecked) {
            TestModuleVersion
        }

        # Verbose output Bicep Version used
        $FullVersion = Get-BicepNetVersion -Verbose:$false
        Write-Verbose -Message "Using Bicep version: $FullVersion"
    }

    process {
        $BicepFile = Get-Childitem -Path $Path -File
    
        # Restore modules
        try {
            Restore-BicepNetFile -Path $BicepFile.FullName -ErrorAction Stop
            Write-Verbose -Message "Successfully restored all modules"
        }
        catch {
            Throw $_
        }
    }
}
#endregion Restore-Bicep

#region Test-BicepFile

# TODO This won't work with BicepNet, has to be rewritten.
function Test-BicepFile {
    [CmdletBinding()]
    param (
        # Specifies a path to one or more locations.
        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true
        )]
        [Alias("PSPath")]
        [ValidateNotNullOrEmpty()]
        [string]
        $Path,

        # Set output type
        [Parameter()]
        [ValidateSet('Simple', 'Json')]
        [String]
        $OutputType = 'Simple',

        # Level of diagnostic that will fail the test
        [Parameter()]
        [BicepDiagnosticLevel]
        $AcceptDiagnosticLevel = [BicepDiagnosticLevel]::Info,


        # Write diagnostic output to screen
        [Parameter()]
        [switch]
        $IgnoreDiagnosticOutput
    )
    
    begin {
        if (-not $Script:ModuleVersionChecked) {
            TestModuleVersion
        }

        if ($VerbosePreference -eq [System.Management.Automation.ActionPreference]::Continue) {
            $FullVersion = Get-BicepNetVersion -Verbose:$false
            Write-Verbose -Message "Using Bicep version: $FullVersion"
        }

        if ($AcceptDiagnosticLevel -eq [BicepDiagnosticLevel]::Error) {
            throw 'Accepting diagnostic level Error results in test never failing.'
        }
    }
    
    process {
        $file = Get-Item -Path $Path
        try {
            $ParseParams = @{
                Path                = $file.FullName 
                InformationVariable = 'DiagnosticOutput' 
                ErrorAction         = 'Stop'
            }
            $BuildResult = Build-BicepNetFile -Path $file.FullName

            if (-not $IgnoreDiagnosticOutput) {
                $BuildResult.Diagnostic | WriteBicepNetDiagnostic -InformationAction 'Continue'
            }
            
        }
        catch {
            # We don't care about errors here.
        }

        $DiagnosticGroups = $BuildResult.Diagnostic | Group-Object -Property { $_.Level }
        $HighestDiagLevel = $null
        foreach ($DiagGroup in $DiagnosticGroups) {
            $Level = [int][BicepDiagnosticLevel]$DiagGroup.Name
            if ($Level -gt $HighestDiagLevel) {
                $HighestDiagLevel = $Level
            }
        }

        switch ($OutputType) {
            'Simple' {
                if ([int]$AcceptDiagnosticLevel -ge $HighestDiagLevel) {
                    return $true
                }
                else {
                    return $false
                }
            }
            'Json' {
                $Result = @{}
                foreach ($Group in $DiagnosticGroups) {
                    $List = foreach ($Entry in $Group.Group) {
                        @{
                            Path      = $Entry.LocalPath
                            Line      = [int]$Entry.Position[0] + 1
                            Character = [int]$Entry.Position[1] + 1
                            Level     = $Entry.Level.ToString()
                            Code      = $Entry.Code
                            Message   = $Entry.Message
                        }
                    }
                    $Result[$Group.Name] = @($List)
                }
                return $Result | ConvertTo-Json -Depth 5
            }
            default {
                # this should never happen but just to make sure
                throw "$_ has not been implemented yet."
            }
        }
    }

}
#endregion Test-BicepFile

#region Uninstall-BicepCLI

function Uninstall-BicepCLI {
    [CmdLetBinding()]
    param (
        [switch]$Force
    )

    if (-not $Script:ModuleVersionChecked) {
        TestModuleVersion
    }
    
    if (-not $IsWindows) {
        Write-Error -Message "This cmdlet is only supported for Windows systems. `
To uninstall Bicep on your system see instructions on https://github.com/Azure/bicep"

        Write-Host "`nList the available module cmdlets by running 'Get-Help -Name Bicep'`n"
        break
    }

    if (TestBicep) {
        # Test if we are running as administrators.
        $currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
        $IsAdmin = $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)

        if (-not $IsAdmin -and -not $Force) {
            Write-Error 'Some Bicep parts might not be properly uninstalled unless you run elevated. Use the -Force switch to try anyway. Any Bicep version installed by Azure CLI to %USERPROFILE%\.Azure\bin will not be uninstalled.'
        }
        if (-not $IsAdmin -and $Force) {
            Write-Host 'You are not running elevated. We may not be able to remove all parts.'
        }
        if ($IsAdmin -or $Force) {
            $UninstallerFileName = 'unins000.exe'
            $BicepExeName = 'bicep.exe'
            $BicepInstalls = $env:Path -split ';' | Where-Object { $_ -like "*\.bicep" -or $_ -like "*\Bicep CLI" }

            foreach ($Install in $BicepInstalls) {
                $FileContents = Get-ChildItem $Install
                if (($UninstallerFileName -in $FileContents.Name) -and ($BicepExeName -in $FileContents.Name)) {
                    # Bicep is installed using installer. Try using it to uninstall
                    $UninstallerPath = ($FileContents | Where-Object -Property Name -eq $UninstallerFileName).FullName
                    & $UninstallerPath /VERYSILENT
                    do {
                        $UninstallProcess = Get-Process -Name $UninstallerFileName.Replace('.exe', '') -ErrorAction SilentlyContinue
                        Start-Sleep -Seconds 1
                    } until ($null -eq $UninstallProcess)
                }
                else {
                    # Bicep is running in standalone exe mode. Remove manualy
                    $ExePath = Join-path -Path $Install -ChildPath $BicepExeName
                    if (Test-Path $ExePath) {
                        Remove-Item $ExePath
                    }
                }
            }

            # verify that no bicep install is still reachable
            $Removed = TestBicep
            if ($Removed) {
                Throw "Unknown version of Bicep is still installed."
            }
            else {
                Write-Host "Successfully removed bicep."
            }

        }
    }
    else {
        Write-Error "Bicep CLI is not installed on this device, nothing to uninstall."
    }
}
#endregion Uninstall-BicepCLI

#region Update-BicepCLI

function Update-BicepCLI {
    [CmdletBinding()]
    param (
    )

    if (-not $Script:ModuleVersionChecked) {
        TestModuleVersion
    }
    
    if (-not $IsWindows) {
        Write-Error -Message "This cmdlet is only supported for Windows systems. `
To update Bicep on your system see instructions on https://github.com/Azure/bicep"

        Write-Host "`nCompare your Bicep version with latest version by running Get-BicepVersion`n"
        break
    }

    $versionCheck = CompareBicepVersion

    if ($versionCheck) {
        Write-Host "You are already running the latest version of Bicep CLI."
    }
    else {
        Uninstall-BicepCLI -Force -ErrorAction SilentlyContinue
        Install-BicepCLI -Force
    }
}
#endregion Update-BicepCLI

#region Update-BicepParameterFile

function Update-BicepParameterFile {
    [CmdletBinding()]
    param (
        [ValidateNotNullOrEmpty()]
        [ValidatePattern('\.json$', ErrorMessage = 'Path must be a parameters file with a .json extension.')]
        [Parameter(Mandatory, Position = 1)]
        [string]$Path,

        [ValidateNotNullOrEmpty()]
        [ValidatePattern('\.bicep$', ErrorMessage = 'BicepFile must have a .bicep extension.')]
        [Parameter(Position = 2)]
        [string]$BicepFile,

        [ValidateNotNullOrEmpty()]
        [Parameter(Position = 3)]
        [ValidateSet('All', 'Required')]
        [string]$Parameters = 'All'
    )

    begin {

        $tempPath = [System.Io.Path]::GetTempPath()

    }
    process {
        
        try {
            $ParamFile = Get-Item -Path $Path -ErrorAction Stop
        }
        catch {
            Write-Error "Cant find ParameterFile at specified Path $Path."
            Break
        }
        
        $FileName = $ParamFile.BaseName.Replace('.parameters', '')
        
        # Try to find a matching Bicep template for the provided parameter file based on its file name
        if (-not $PSBoundParameters.ContainsKey('BicepFile')) {
            $BicepFilePath = (Get-ChildItem $ParamFile.DirectoryName -Filter *.bicep | Where-Object { $_.BaseName -eq $FileName }).FullName
            
            if (-not $BicepFilePath) {
                Write-Error "Cant find BicepFile Named $FileName in directory: $($ParamFile.DirectoryName)"
                Break
            }
        }
        else {
            if (-not (Test-Path $BicepFile)) {
                Write-Error "Cant find BicepFile at specified Path $BicepFile."
                Break
            }
            $BicepFilePath = $BicepFile
        }
        
        $validateBicepFile = Test-BicepFile -Path $BicepFilePath -AcceptDiagnosticLevel Warning -IgnoreDiagnosticOutput
        if (-not $validateBicepFile) {
            Write-Error -Message "$BicepFilePath have build errors, make sure that the Bicep template builds successfully and try again."
            Write-Host "`nYou can use either 'Test-BicepFile' or 'Build-Bicep' to verify that the template builds successfully.`n"
            break
        }

        # Import the old parameter file and convert it to an ordered hashtable
        $oldParametersFile = Get-Content -Path $Path | ConvertFrom-Json -Depth 100 | ConvertToHashtable -Ordered

        # Generate a temporary Bicep Parameter File with all parameters
        $BicepFileName = (Get-Item -Path $BicepFilePath).BaseName
        New-BicepParameterFile -Path $BicepFilePath -OutputDirectory $tempPath -Parameters All

        $allParametersFilePath = $tempPath + "$($BicepFileName).parameters.json"

        # Convert the all parameters file to an ordered hashtable
        try {
            $allParametersFile = Get-Content -Path $allParametersFilePath -ErrorAction Stop | ConvertFrom-Json -Depth 100 | ConvertToHashtable -Ordered
        }
        catch {
            Write-Error "Failed to create Bicep ParameterObject."
            Break
        }        

        # Remove any deleted parameters from old parameter file
        $oldParameterArray = @()
        $oldParametersFile.parameters.Keys.ForEach( { $oldParameterArray += $_ })
        
        foreach ($item in $oldParameterArray) {
            if (-not $allParametersFile.parameters.Contains($item)) {
                $oldParametersFile.parameters.Remove($item)
            }
        }
        
        
        # Generate a new temporary Bicep Parameter File if -Parameters parameter is set to Required
        if ($Parameters -eq 'Required') {
            $BicepFileName = (Get-Item -Path $BicepFilePath).BaseName
            New-BicepParameterFile -Path $BicepFilePath -OutputDirectory $tempPath -Parameters $Parameters
        }
        $NewParametersFilePath = $tempPath + "$BicepFileName.parameters.json"
        
        # Convert the new parameter file to an ordered hashtable
        try {
            $NewParametersFile = Get-Content -Path $NewParametersFilePath -ErrorAction Stop | ConvertFrom-Json -Depth 100 | ConvertToHashtable -Ordered
        }
        catch {
            Write-Error "Failed to create Bicep ParameterObject."
            Break
        }
        
        $ParameterArray = @()
        $NewParametersFile.parameters.Keys.ForEach( { $ParameterArray += $_ })
        
        # Iterate over the new parameters and add any missing to the old parameters array
        foreach ($item in $ParameterArray) {
            if (-not $oldParametersFile.parameters.Contains($item)) {
                $oldParametersFile.parameters[$item] = @{                                
                    value = $NewParametersFile.parameters.$item.value
                }
            }
        }
        $oldParametersFile | ConvertTo-Json -Depth 100 | Out-File -Path $Path -Force
        
    }
    end {
        Remove-Item $NewParametersFilePath -Force
    }
}
#endregion Update-BicepParameterFile

#region Update-BicepTypes

function Update-BicepTypes {
    [CmdletBinding(SupportsShouldProcess)]
    param ()

    if (-not $Script:ModuleVersionChecked) {
        TestModuleVersion
    }

    $ModulePath = (Get-Module Bicep).Path
    $ModuleFolder = Split-Path -Path $ModulePath

    # Where the file is stored
    $BicepTypesPath = Join-Path -Path $ModuleFolder -ChildPath 'Assets\BicepTypes.json'
    
    # Url to fetch new types from
    $BicepTypesUrl = 'https://raw.githubusercontent.com/Azure/bicep-types-az/main/generated/index.json'
    
    Write-Verbose "Fetching types from GitHub: $BicepTypesUrl"
    
    try {
        $BicepTypes = Invoke-RestMethod -Uri $BicepTypesUrl -Verbose:$false
    }
    catch {
        Throw "Unable to get new Bicep types from GitHub. $_"
    }

    Write-Verbose "Filtering content"
    
    # If the Resources property does not exist we want to throw an error
    if ($BicepTypes.psobject.Properties.name -notcontains 'Resources') {
        Throw "Resources not found in index file."
    }

    try {
        $TypesOnly = ConvertTo-Json -InputObject $BicepTypes.Resources.psobject.Properties.name -Compress
    }
    catch {
        Throw "Unable to filter content. Index file might have changed. $_"
    }

    Write-Verbose "Saving to disk"
    
    try {
        Out-File -FilePath $BicepTypesPath -InputObject $TypesOnly -WhatIf:$WhatIfPreference
    }
    catch {
        Throw "Failed to save new Bicep types. $_"
    }

    if (-not $WhatIfPreference) {
        Write-Host "Updated Bicep types."
    }

    # To avoid having to re-import the module, update the module variable
    $null = GetBicepTypes -Path $BicepTypesPath
}
#endregion Update-BicepTypes