src/private/ConvertTo-DOTLangauge.ps1

function ConvertTo-DOTLanguage {
    [CmdletBinding()]
    param (
        [string[]] $Targets,
        [ValidateSet('Azure Resource Group')]
        [string] $TargetType = 'Azure Resource Group',
        [int] $LabelVerbosity = 1,
        [int] $CategoryDepth = 1,
        [string] $Direction = 'top-to-bottom',
        [string] $Splines = 'spline',
        [string[]] $ExcludeTypes
    )
    
    begin {
        switch ($Direction) {
            'left-to-right' { $rankdir = "LR" }
            'top-to-bottom' { $rankdir = "TB" }
        }
    }
    
    process {

        if (!(Test-AzLogin)) {
            break
        }
        
        $SpecialChars = '() []{}&-.'
        $GraphObjects = @()
        $NetworkObjects = ConvertFrom-Network -TargetType $TargetType -Targets $Targets -CategoryDepth $CategoryDepth -ExcludeTypes $ExcludeTypes
        $GraphObjects += $NetworkObjects
        $ARMObjects = ConvertFrom-ARM -TargetType $TargetType -Targets $Targets -CategoryDepth $CategoryDepth -ExcludeTypes $ExcludeTypes
        $GraphObjects += $ARMObjects

        $GraphObjects = $GraphObjects | 
        Group-Object Name | 
        ForEach-Object {
            [PSCustomObject]@{
                Name      = $_.Name
                Type      = $_.group.type | Select-Object -First 1
                Resources = $_.group.Resources
            }
        } | 
        Sort-Object Rank
        
        $Counter = 0
        $subgraphs = foreach ($Target in $GraphObjects) {
            $Counter = $Counter + 1              
            Write-CustomHost "Plotting sub-graph for $($Target.Type): `"$($Target.Name)`"" -Indentation 1 -color Green -AddTime

            $VNets = Get-AzVirtualNetwork -ResourceGroupName $Target.Name -Verbose:$false
            $NetworkLayout = @()
            if ($VNets) {
                
                $VNetCounter = 0
                $VMs_and_NICs = @()
                $VMs = Get-AzVM -ResourceGroupName $Target.Name -Verbose:$false
                $NICs = Get-AzNetworkInterface -ResourceGroupName $Target.Name -Verbose:$false
                $VMs_and_NICs += $VMs
                $VMs_and_NICs += $NICs
                
                foreach ($vnet in $VNets) {
                    
                    $VNetCounter = $VNetCounter + 1

                    $VNetLabel = Get-ImageLabel -Type "Microsoft.Network/virtualNetworks" -Row1 "$($VNet.Name)" -Row2 "$([string]$VNet.AddressSpace.AddressPrefixes)"
                    $VNetSubGraphName = Remove-SpecialChars -String $VNet.Name -SpecialChars $SpecialChars
                    $VNetSubGraphAttributes = @{
                        label    = $VNetLabel;
                        labelloc = 't';
                        penwidth = "1";
                        fontname = "Courier New" ;
                        style    = "rounded,dashed";
                        color    = $VNetGraphColor
                        bgcolor  = $VNetGraphBGColor
                    }
    
                    # generating dot language for virtual networks and then iterating all the subnets
                    $NetworkLayout += SubGraph -Name $VNetSubGraphName -Attributes $VNetSubGraphAttributes -ScriptBlock {
                        $Subnets = $VNet.Subnets

                        # if there a no subnets in a virtual network, then plot empty vNet
                        # if(!$Subnets){

                        # }
                        # else{
                        foreach ($subnet in $Subnets) {
    
                            $SubnetLabel = Get-ImageLabel -Type "Subnets" -Row1 "$($Subnet.Name)" -Row2 "$([string]$Subnet.AddressPrefix)"
                            $SubnetSubGraphName = Remove-SpecialChars -String $Subnet.Name -SpecialChars $SpecialChars
                            $SubnetSubGraphAttributes = @{
                                label    = $SubnetLabel;
                                labelloc = 't';
                                penwidth = "1";
                                fontname = "Courier New" ;
                                style    = "rounded,dashed";
                                color    = $SubnetGraphColor
                                bgcolor  = $SubnetGraphBGColor; 
                            }
    
                            # generating dot language for subnets inside virtual networks
                            SubGraph -Name $SubnetSubGraphName -Attributes $SubnetSubGraphAttributes -ScriptBlock {    
                                $resources_in_subnet = foreach ($item in $VMs_and_NICs) {
                                    switch ($item.Type) {
                                        'Microsoft.Compute/virtualMachines' {
                                            $networkInterface = $NICs.Where( { $_.name -eq ($item.NetworkProfile.NetworkInterfaces[0].Id.Split('/')[-1]) })
                                            $subnetName = $networkInterface.IpConfigurations[0].Subnet.Id.split('/')[-1] 
                                        }
                                        'Microsoft.Network/networkInterfaces' {
                                            $subnetName = $item.IpConfigurations[0].Subnet.Id.split('/')[-1] 
                                        }
                                    }
        
                                    if ($subnetName -eq $subnet.Name) {
                                        $item | Select-Object Name, Type
                                    }
                                }
        
                                $resources_in_subnet |
                                ForEach-Object {
                                    Get-ImageNode -Name "$($_.Type)/$($_.Name)".tolower() -Rows $_.Name -Type $_.Type
                                }
                            }
                        }
                        # }

                    }
                }
            }

            #region plotting-edges-to-nodes
            $Resources = $Target.Resources
            if ($Resources) {
                # $NodesAndEdges = $Resources |
                $nodes = @()
                $edges = @()

                $Resources |
                Tee-Object -Variable pipe_var |
                ForEach-Object {
                    $from = $_.from
                    $fromcateg = $_.fromcateg
                    $to = $_.to
                    $tocateg = $_.tocateg
                    if ($_.isdependent) {
                        $edges += Edge -From "$fromcateg/$from".tolower() `
                                        -to "$tocateg/$to".tolower() `
                                        -Attributes @{
                                            arrowhead = 'normal';
                                            style     = 'dashed';
                                            # label = 'dependsOn'
                                            penwidth  = "1"
                                            fontname  = "Courier New"
                                            color     = $DependencyEdgeColor
                                        }
    
                        if ($LabelVerbosity -eq 1) {
                            $nodes += Get-ImageNode -Name "$fromcateg/$from".tolower() -Rows $from -Type $fromcateg -ErrorAction SilentlyContinue
                            $nodes += Get-ImageNode -Name "$tocateg/$to".tolower() -Rows $to -Type $tocateg -ErrorAction SilentlyContinue
                        }
                        elseif ($LabelVerbosity -eq 2) {
                            $nodes += Get-ImageNode -Name "$fromcateg/$from".tolower() -Rows ($from, $fromcateg) -Type $fromcateg -ErrorAction SilentlyContinue
                            $nodes += Get-ImageNode -Name "$tocateg/$to".tolower() -Rows ($to, $toCateg) -Type $tocateg -ErrorAction SilentlyContinue
                        }
                    }
                    if ($_.association) {
                        $edges += Edge -From "$fromcateg/$from".tolower() `
                            -to "$tocateg/$to".tolower() `
                            -Attributes @{
                            arrowhead = 'normal';
                            style     = 'solid';
                            penwidth  = "1"
                            fontname  = "Courier New"
                            color     = $NetworkEdgeColor
                        }
    
                        if ($LabelVerbosity -eq 1) {
                            $nodes += Get-ImageNode -Name "$fromcateg/$from".tolower() -Rows $from -Type $fromcateg -ErrorAction SilentlyContinue
                            $nodes += Get-ImageNode -Name "$tocateg/$to".tolower() -Rows $to -Type $tocateg -ErrorAction SilentlyContinue
                        }
                        elseif ($LabelVerbosity -eq 2) {
                            $nodes += Get-ImageNode -Name "$fromcateg/$from".tolower() -Rows ($from, $fromcateg) -Type $fromcateg -ErrorAction SilentlyContinue
                            $nodes += Get-ImageNode -Name "$tocateg/$to".tolower() -Rows ($to, $toCateg) -Type $tocateg -ErrorAction SilentlyContinue
                        }
                    }
                    else {
                        if ($LabelVerbosity -eq 1) {
                            $nodes += Get-ImageNode -Name "$fromcateg/$from".tolower() -Rows $from -Type $fromcateg -ErrorAction SilentlyContinue
                        }
                        elseif ($LabelVerbosity -eq 2) {
                            $nodes += Get-ImageNode -Name "$fromcateg/$from".tolower() -Rows ($from, $fromcateg) -Type $fromcateg -ErrorAction SilentlyContinue
                        }
                    }
                } | 
                Select-Object -Unique

                if($nodes){

                    Write-CustomHost -String "Creating Nodes" -Indentation 2 -color Green

                    $nodes |
                    Select-Object -Unique | 
                    ForEach-Object {
                        Write-CustomHost -String $($_.split(" ")[0].Replace('"','')) -Indentation 3 -color Green
                    } 
                }

                if($edges){

                    Write-CustomHost -String "Creating Edges" -Indentation 2 -color Green

                    $edges |
                    Select-Object -Unique | 
                    ForEach-Object {
                        $first, $second = $_.split(" ")[0].Replace('"','').split("->")
                        if($first -and $second){
                            Write-CustomHost -String "$first -> $second" -Indentation 3 -color Green
                        }
                    }
                }

                $NodesAndEdges = $nodes + $edges
            }

            if ($Resources -or $VNets) {
                $ResourceGroupLocation = (Get-AzResourceGroup -Name $Target.Name -Verbose:$false).Location
                $ResourceGroupSubGraphName = [string]::Concat($(Remove-SpecialChars -String $Target.Name -SpecialChars $SpecialChars), $Counter)
                $ResourceGroupSubGraphNameLabel = Get-ImageLabel -Type "ResourceGroups" -Row1 "ResourceGroup: $(Remove-SpecialChars -String $Target.name -SpecialChars $SpecialChars)" -Row2 "Location: $($ResourceGroupLocation)"
                $ResourceGroupSubGraphAttributes = @{
                    label    = $ResourceGroupSubGraphNameLabel;
                    labelloc = 't';
                    penwidth = "1";
                    fontname = "Courier New" ;
                    style    = "rounded, dashed";
                    color    = $ResourceGroupGraphColor;
                    bgcolor  = $ResourceGroupGraphBGColor;
                    fontsize = "9"; 
                }

                SubGraph -Name $ResourceGroupSubGraphName -Attributes $ResourceGroupSubGraphAttributes -ScriptBlock {
                    $NetworkLayout
                    $NodesAndEdges
                }
            } else {
                Write-CustomHost -String "No resources found.. re-run the command and try increasing the category depth using -CategoryDepth 2 or -CategoryDepth 3 cmdlet parameters." -Indentation 1 -Color Red -AddTime
            }
            #endregion plotting-edges-to-nodes
        }

        $Legend = @()
        $Legend += ' subgraph clusterLegend {'
        $Legend += ' label = "Legend\n\n";'
        $Legend += ' rank = 9999999999999'
        $Legend += ' clusterrank=local'
        $Legend += ' bgcolor = {0}' -f $MainGraphBGColor
        $Legend += ' fontcolor = {0}' -f $EdgeFontColor
        $Legend += ' fontsize = 11'
        $Legend += ' node [shape=point]'
        $Legend += ' {'
        $Legend += ' rank=same'
        $Legend += ' d0 [style = invis];'
        $Legend += ' d1 [style = invis];'
        $Legend += ' p0 [style = invis];'
        $Legend += ' p1 [style = invis];'
        $Legend += ' }'
        $Legend += ' d0 -> d1 [arrowhead="normal";style="dashed";label="Resource\nDependency";color="{0}";fontname="Courier New";penwidth="1";fontsize="9";fontcolor="{1}"]' -f $DependencyEdgeColor, $EdgeFontColor
        $Legend += ' p0 -> p1 [style="solid";fontname="Courier New";label="Network\nAssociation";arrowhead="normal";color="{0}";penwidth="1";fontsize="9";fontcolor="{1}"]' -f $NetworkEdgeColor, $EdgeFontColor
        $Legend += ' }'



        if ($subgraphs) {
            $Subscription = (Get-AzContext).Subscription
            $VisualizationAttributes = @{
                rankdir   = $rankdir
                overlap   = 'false'
                splines   = $Splines 
                color     = $VisualizationGraphColor
                bgcolor   = $VisualizationGraphColor
                penwidth  = "1"
                fontname  = "Courier New" 
                fontcolor = $GraphFontColor
                fontsize  = "9"
            }

            $graph = Graph -Name 'Visualization' -Attributes $VisualizationAttributes -ScriptBlock {
                
                $MainGraphLabel = Get-ImageLabel -Type "Subscriptions" -Row1 "Subscription: $(Remove-SpecialChars -String $Subscription.name -SpecialChars $SpecialChars)" -Row2 "Id: $($Subscription.Id)"
                $MainGraphAttributes = @{
                    label    = $MainGraphLabel
                    fontsize = "9"
                    style    = "rounded,solid"
                    bgcolor  = $MainGraphBGColor
                }

                SubGraph -Name 'main' -Attributes $MainGraphAttributes -ScriptBlock {
            
                    edge @{color = $EdgeColor; fontcolor = $EdgeFontColor; fontsize = "11" }
                    node @{color = $NodeColor ; fontcolor = $NodeFontColor; fontsize = "11" }
                
                    $subgraphs
                }
                
                # graph legends only appear if edges exist between the nodes
                # legends for now only represent the edges in the graph and if there are no edges there is no point showing legends in the graph
                if ($edges){
                    $Legend
                }
            }

            # hack to fix issue because of double-quotes in image labels
            $graph = $graph | ForEach-Object {
                if ($_ -like '*"<<TABLE*') {
                    $_.replace('"', '')
                }
                else {
                    $_
                }
            } 

            # to indent and validate the dot language generated by powershell
            $GraphViz = Get-DOTExecutable

            if ( $null -eq $GraphViz ) {
                Write-Error "'GraphViz' is not installed on this system and is a prerequisites for this module to work. Please download and install from here: https://graphviz.org/download/ and re-run this command." -ErrorAction Stop
            }
            else {
                $dot_file = (Join-Path ([System.IO.Path]::GetTempPath()) "temp.dot")
                $graph | Out-String | Out-File $dot_file -Verbose:$false -Encoding ascii
                if (Test-Path $dot_file) {
                    if ($IsLinux) {
                        Invoke-Expression "$($GraphViz.FullName) $dot_file"
                    }
                    else {
                        & $GraphViz.FullName $dot_file
                    }

                    Remove-Item $dot_file -Force
                }
                else {
                    $graph | Out-String 
                }
            }

        }
    }
    
    end {
        
    }
}