<#
Documentation for Intune 

This module contains the base documentation function for document objects in Intune

This module will document Settings objects (Settings Catalog, Endpoint Security and Administrative Templates) and 
property objects like Configuration Profiles and Comliance Policies

Property objectes are documented based on the ObjectCategories.json file and the json files in the Documentation\ObjectInfo folder.
These json files contains the definition of each property of an object

Settings objects are documented with MS Graph APIs.

A basic Output provider is included that exports the objects to a CSV file

#>

$global:documentationOutput = @()
$global:documentationProviders = @()

function Get-ModuleVersion
{
    '2.3.0'
}

function Invoke-InitializeModule
{
    $script:alwaysUseMigTableForTranslation = $false

    # Make sure we add the default Output types
    Add-OutputType

    $script:columnHeaders = @{
        Name="Inputs.displayNameLabel"
        Value="TableHeaders.value"
        Description="TableHeaders.description"
        GroupMode="SettingDetails.modeTableHeader" #assignmentTypeSelectionLabel?
        Group="TableHeaders.assignedGroups"
        Groups="TableHeaders.groups"
        useDeviceContext="SettingDetails.installContextLabel"
        uninstallOnDeviceRemoval="SettingDetails.UninstallOnRemoval"
        isRemovable="SettingDetails.installAsRemovable"
        vpnConfigurationId="PolicyType.vpn"
        Action="SettingDetails.actionColumnName"
        Schedule="ScheduledAction.List.schedule"
        MessageTemplate="ScheduledAction.Notification.messageTemplate"
        EmailCC="ScheduledAction.Notification.additionalRecipients"
        Rule="ApplicabilityRules.GridLabel.Rule"
        ValueWithLabel="TableHeaders.value"
        Status="TableHeaders.status"
        CombinedValueWithLabel="TableHeaders.value"
        CombinedValue="TableHeaders.value"
        useDeviceLicensing="TableHeaders.licenseType"
        Filter="AppResources.AppSettingsUx.assignmentFilterColumnHeader"
        filterMode="AppResources.AppSettingsUx.assignmentFilterTypeColumnHeader"
        deliveryOptimizationPriority="AppResources.AppSettingsUx.deliveryOptimizationPriorityHeader"
        startTimeColumnLabel="AppResources.AppSettingsUx.startTimeColumnLabel"
        installTimeSettings="AppResources.AppSettingsUx.deadlineTimeColumnLabel"
        restartSettings="AppResources.AppSettingsUx.restartGracePeriodHeader"
        notifications="AppResources.AppSettingsUx.assignmentToast"
        Settings="TableHeaders.settings"
        returnCode='Win32ReturnCodes.Columns.returnCode'
        type='Win32ReturnCodes.Columns.codeType'
        RecommendedValue="AzureIAMCommon.Recommended"
    }    
}

function Invoke-ShowMainWindow
{
    $button = [System.Windows.Controls.Button]::new()
    $button.Content = "Document"
    $button.Name = "btnDocument"
    $button.MinWidth = 100
    $button.Margin = "0,0,5,0" 
    $button.IsEnabled = $false
    $button.ToolTip = "Document selected objects"

    $button.Add_Click({ 

        $objects = ?? ($global:dgObjects.ItemsSource | Where IsSelected -eq $true) $global:dgObjects.SelectedItem 

        Show-DocumentationForm -Objects $objects
    })    

    $global:spSubMenu.RegisterName($button.Name, $button)

    $global:spSubMenu.Children.Insert(0, $button)
}

function Invoke-EMSelectedItemsChanged
{
    $hasSelectedItems = ($global:dgObjects.ItemsSource | Where IsSelected -eq $true) -or ($null -ne $global:dgObjects.SelectedItem)
    Set-XamlProperty $global:dgObjects.Parent "btnDocument" "IsEnabled" $hasSelectedItems
}

function Invoke-GraphObjectsChanged
{
    $btnDocument = $global:spSubMenu.Children | Where-Object { $_.Name -eq "btnDocument" }
    $btnExport = $global:spSubMenu.Children | Where-Object { $_.Name -eq "btnExport" }
    if($btnDocument -and $btnExport)
    {
        $btnDocument.Visibility = $btnExport.Visibility 
    }
}

function Invoke-ViewActivated
{
    if($global:currentViewObject.ViewInfo.ID -ne "IntuneGraphAPI") { return }
    
    $tmp = $mnuMain.Items | Where Name -eq "EMBulk"
    if($tmp)
    {
        $tmp.AddChild(([System.Windows.Controls.Separator]::new())) | Out-Null
        $subItem = [System.Windows.Controls.MenuItem]::new()
        $subItem.Header = "_Document Types"
        $subItem.Add_Click({Invoke-DocumentObjectTypes})
        $tmp.AddChild($subItem)

        $subItem = [System.Windows.Controls.MenuItem]::new()
        $subItem.Header = "D_ocument Selected"
        $subItem.Add_Click({Invoke-DocumentSelectedObjects})
        $tmp.AddChild($subItem)       
    }
}

function Set-DocColumnHeaderLanguageId
{
    param($columnName, $lngId)

    if(-not $script:columnHeaders -or -not $lngId) { return }

    if($script:columnHeaders.ContainsKey($columnName))
    {
        $script:columnHeaders[$columnName] = $lngId
    }
    else
    {
        $script:columnHeaders.Add($columnName, $lngId)
    }
}

function Invoke-DocTranslateColumnHeader
{
    param($columnName)

    $lngText = ""
    if($script:columnHeaders.ContainsKey($columnName))
    {
        $lngText = Get-LanguageString $script:columnHeaders[$columnName]
    }

    (?? $lngText $columnName)
}

function Add-OutputType
{
    param($outputInfo)

    if(-not $global:documentationOutput)
    {
        $global:documentationOutput = @()
    }

    if($global:documentationOutput.Count -eq 0)
    {
        $global:documentationOutput += [PSCustomObject]@{
            Name = "None (Raw output only)"
            Value = "none"
        }

        $global:documentationOutput += [PSCustomObject]@{
            Name="CSV"
            Value="csv"
            OutputOptions = (Add-CSVOptionsControl)
            Activate = { Invoke-CSVActivate @args }
            PreProcess = { Invoke-CSVPreProcessItems @args }
            Process = { Invoke-CSVProcessItem @args }
        }        
    }

    if(!$outputInfo) { return }

    $global:documentationOutput += $outputInfo
}

function Add-DocumentationProvicer
{
    param($docProvider)

    if(-not $global:documentationProviders)
    {
        $global:documentationProviders = @()
    }

    $global:documentationProviders += $docProvider
}

function Get-ObjectDocumentation
{
    param($documentationObj)

    $additionalInfo = ""
    if($documentationObj.Object.'@ObjectFromFile' -eq $true)
    {
        $additionalInfo = " - From File"
    }

    Write-Status "Get documentation info for $((Get-GraphObjectName $documentationObj.Object $documentationObj.ObjectType)) ($($documentationObj.ObjectType.Title))$additionalInfo"

    $status = $null
    $inputType = "Settings"    

    if(-not $script:scopeTags -and $script:offlineDocumentation -ne $true)
    {
        $script:scopeTags = (Invoke-GraphRequest -Url "/deviceManagement/roleScopeTags").Value
    }   

    $obj = $documentationObj.Object
    $objectType = $documentationObj.ObjectType
    $script:currentObject = $obj

    $script:languageStrings = $null
 
    $script:CurrentSubCategory = $null
    $script:objectBasicInfo = @()
    $script:objectSettingsData = @()
    $script:objectComplianceActionData = @()
    $script:applicabilityRules = @()
    $script:objectAssignments = @()
    $script:objectScripts = @()
    $script:customTables = @()
    $script:admxCategories = $null

    $script:ObjectTypeFullTable = @{} # Hash table with objects that should be documented in a single table eg ScopeTags

    $updateFilteredObject = $true

    $type = $obj.'@OData.Type'

    $defaultDocumentationProperties =  @("Name","Value")

    $retObj = $null
    foreach($docProvider in ($global:documentationProviders | Sort -Property Priority))
    {
        if($docProvider.DocumentObject)
        {
            $retObj = & $docProvider.DocumentObject $documentationObj
            if($retObj -ne $null)
            {
                break
            }
        }
    }
    
    $propertyObjectProperties = @("Name","Value","Category","SubCategory","RawValue","RawJsonValue","DefaultValue","UnconfiguredValue","EntityKey","Description","Enabled")

    #region Custom documentation
    if($retObj)
    {
        $status =  $retObj.ErrorText
        $properties = $retObj.Properties
        $inputType = ?? $retObj.InputType "Property"
        $defaultDocumentationProperties = ?? $retObj.DefaultDocumentationProperties $defaultDocumentationProperties
    }
    #endregion
    #region Manually created file - ODataType
    elseif([IO.File]::Exists(($global:AppRootFolder + "\Documentation\ObjectInfo\$($obj.'@OData.Type').json")))
    {
        $inputType = "Property"
        # Process object based on OData type
        Invoke-TranslateCustomProfileObject $obj "$($obj.'@OData.Type')" | Out-Null
        $properties = $propertyObjectProperties
    }
    #endregion
    #region Manually created file - ObjectType id
    elseif($objectType -and [IO.File]::Exists(($global:AppRootFolder + "\Documentation\ObjectInfo\#$($objectType.Id).json")))
    {
        $inputType = "Property"
        # Process object based on Intune Object Type ($objectType)
        # '#' is added to front of name to distinguish manually created files from generated files
        Invoke-TranslateCustomProfileObject $obj "#$($objectType.Id)" | Out-Null
        $properties = $propertyObjectProperties
    }    
    #endregion
    #region Settings Catalog
    elseif($type -eq "#microsoft.graph.deviceManagementConfigurationPolicy")
    {  
        Invoke-TranslateSettingsObject $obj $objectType | Out-Null
        $properties = @("Name","Value","RootCategory","Category","RawValue","RawJsonValue","DefaultValue","Description")
    }
    #endregion
    #region Compliance Policies V2
    elseif($type -eq "#microsoft.graph.deviceManagementCompliancePolicy")
    {  
        Invoke-TranslateComplianceV2Object $obj $objectType | Out-Null
        $properties = @("Name","Value","RootCategory","Category","RawValue","RawJsonValue","DefaultValue","Description")
    }
    #endregion
    #region Endpoint Security
    elseif($type -eq "#microsoft.graph.deviceManagementIntent")
    {
        Invoke-TranslateIntentObject $obj $objectType | Out-Null
        $properties = @("Name","Value","Category","FullValueTable","RawValue","RecommendedValue","SettingId","Description")
        $defaultDocumentationProperties = @("Name","Value","RecommendedValue")
        $inputType = "Intent"
    }
    #endregion
    #region Administrative Templates
    elseif($type -eq "#microsoft.graph.groupPolicyConfiguration")
    {
        Invoke-TranslateADMXObject $obj $objectType | Out-Null
        $properties = @("Name","Status","Value","Category","CategoryPath","RawValue","ValueWithLabel","Created","Modified", "Class", "DefinitionId")
        $defaultDocumentationProperties =  @("Name","Status",(?? $script:ValueOutputProperty "Value"))
        $updateFilteredObject = $false
        $inputType = "Property"
    }
    #endregion
    #region Profile Types e.g. DeviceConfiguration Policies etc
    elseif($type)
    {
        $inputType = "Property"
        $processed = $true

        
        $processed = Invoke-TranslateProfileObject $obj

        if($processed -eq $false) 
        {
            $errText = "No object file or object info found for $((Get-GraphObjectName $obj $objType)) ($($obj.'@OData.Type'))`n`nObject type not supported for documentation"
            # Object not processed
            $status = $errText
            Write-Log $errText 3             
        }
        $properties = $propertyObjectProperties
    }
    #endregion


    if($objectType.DocumentAll -eq $true) { return }

    if($script:objectBasicInfo.Count -gt 0)
    {
        Add-ScopeTagStrings $obj
    }

    if($obj.scheduledActionsForRule.scheduledActionConfigurations)
    {
        Invoke-TranslateScheduledActionType $obj.scheduledActionsForRule
    }    

    if(($obj.assignments | measure).Count -gt 0)
    {
        Invoke-TranslateAssignments $obj
    }

    $script:settingsProperties = $properties

    $objectDocumentationInfo = [PSCustomObject]@{
        BasicInfo = $script:objectBasicInfo
        Settings = $script:objectSettingsData
        ComplianceActions = $script:objectComplianceActionData
        ApplicabilityRules = $script:applicabilityRules
        CustomTables = $script:customTables
        Assignments = $script:objectAssignments
        Scripts = $script:objectScripts
        DisplayProperties = $properties
        DefaultDocumentationProperties = $defaultDocumentationProperties
        ErrorText = $status
        InputType = $inputType
        UpdateFilteredObject = $updateFilteredObject
        UnconfiguredProperties = $obj."@UnconfiguredProperties"
    }
    
    foreach($docProvider in ($global:documentationProviders | Sort -Property Priority))
    {
        if($docProvider.ObjectDocumented)
        {
            & $docProvider.ObjectDocumented $obj $objectType $objectDocumentationInfo
        }
    }    

    $objectDocumentationInfo
}

function Get-DocumentedSettings
{
    $script:objectSettingsData
}

function Invoke-ObjectDocumentation
{
    param($documentationObj,
        $ObjectSeparator,
        $PropertySeparator,
        $DocumentationLanguage
        )

    $global:intentCategories = $null
    $global:catRecommendedSettings = $null
    $global:intentCategoryDefs = $null
    $global:cfgCategories = $null
    $script:admxCategories = $null
    $script:migTable = $null
    $script:offlineObjects = @{}

    $script:DocumentationLanguage = ?? $DocumentationLanguage "en"
    $script:objectSeparator = ?? $ObjectSeparator ([System.Environment]::NewLine)
    $script:propertySeparator = ?? $PropertySeparator ","

    $loadExportedInfo = $false

    if($documentationObj.Object."@ObjectFileName") {
        $path = [IO.Path]::GetDirectoryName($documentationObj.Object."@ObjectFileName")
        for($i = 0;$i -lt 2;$i++)
        {
            if($i -gt 0)
            {
                # Get parent directory
                $path = [io.path]::GetDirectoryName($path)
            }
    
            $migFileName = Join-Path $path "MigrationTable.json"
            try
            {
                if([IO.File]::Exists($migFileName))
                {
                    Write-Log "Load Migration table from $migFileName"
                    $script:migTable = ConvertFrom-Json (Get-Content $migFileName -Raw)        
                }
            }
            catch {}
        }
        if(-not $script:migTable)  {
            Write-Log "Migration table not found" 2
        }
    }

    Get-ObjectDocumentation $documentationObj
}

function Add-RawDataInfo
{
    param($obj, $objectType)

    $params = @{}

    if($txtDocumentationRawData.Text)
    {
        $txtDocumentationRawData.Text += "`n`n"    
    }

    $txtDocumentationRawData.Text += "#########################################################" 
    $txtDocumentationRawData.Text += "`n`n# Object: $((Get-GraphObjectName $obj $objectType))" 
    $txtDocumentationRawData.Text += "`n# Type: $($obj.'@OData.Type')" 
    $txtDocumentationRawData.Text += "`n`n#########################################################" 

    if($script:objectBasicInfo.Count -gt 0)
    {
        $txtDocumentationRawData.Text += "`n`n# Basic Info`n`n" 
        $txtDocumentationRawData.Text += (($script:objectBasicInfo | Select -Property Name,Value | ConvertTo-Csv -NoTypeInformation @params) -join ([System.Environment]::NewLine))
    }

    if($script:objectSettingsData)
    {
        $txtDocumentationRawData.Text += "`n`n# Object Settings`n`n" 
        $txtDocumentationRawData.Text += (($script:objectSettingsData | Select -Property $script:settingsProperties | ConvertTo-Csv -NoTypeInformation @params) -join ([System.Environment]::NewLine))
    }

    if(($documentedObj.ApplicabilityRules | measure).Count -gt 0)
    {
        $txtDocumentationRawData.Text += "`n`n# Applicability rules`n`n" 
        $txtDocumentationRawData.Text += (($script:applicabilityRules | Select Rule,Property,Value,Category | ConvertTo-Csv -NoTypeInformation) -join ([System.Environment]::NewLine))
    }    

    if($script:objectComplianceActionData.Count -gt 0)
    {
        $txtDocumentationRawData.Text += "`n`n# Compliance actions`n`n" 
        $txtDocumentationRawData.Text += (($script:objectComplianceActionData | Select Action,Schedule,MessageTemplate,EmailCC,Category | ConvertTo-Csv -NoTypeInformation) -join ([System.Environment]::NewLine))
    }

    if($script:objectAssignments.Count -gt 0)
    {
        if($script:objectAssignments.Count -gt 0 -and $script:objectAssignments[0].RawIntent)
        {
            $properties = @("GroupMode","Group","Filter","FilterMode","Category","SubCategory","RawIntent")
        }
        else
        {
            $properties = @("GroupMode","Group","Filter","FilterMode","Category")
        }

        $txtDocumentationRawData.Text += "`n`n# Assignments`n`n" 
        $txtDocumentationRawData.Text += (($script:objectAssignments | Select -Property $properties | ConvertTo-Csv -NoTypeInformation) -join ([System.Environment]::NewLine))
    }
}

function Get-ObjectTypeGroupName
{
    param($objectType)

    if($objectType.Id -eq "DeviceConfiguration")
    {
        return (Get-LanguageString "PolicySelection.Templates.title")
    }
    elseif($objectType.Id -eq "ConditionalAccess")
    {
        return (Get-LanguageString "TermsOfUse.Details.Tab.cAPolicies")
    }
    else
    {
        return $ObjectType.Title
    }

}

function Get-ObjectTypeString
{
    param($obj, $objectType)

    $objTypeId = ?? $objectType.GroupId $objectType
    
    if($objTypeId -eq "DeviceConfiguration")
    {
        return (Get-LanguageString "SettingDetails.deviceConfigurationTitle")
        #!!!return (Get-LanguageString "PolicySelection.Templates.title")
        
    }
    elseif($objTypeId -eq "CompliancePolicies")
    {
        return (Get-LanguageString "SettingDetails.deviceComplianceTitle")
    }
    elseif($objTypeId -eq "Apps")
    {
        return (Get-LanguageString "SettingDetails.clientAppsTitle")
    }
    elseif($objTypeId -eq "WinEnrollment")
    {
        return (Get-LanguageString "SettingDetails.windowsEnrollmentTitle")
    }
    elseif($objTypeId -eq "AppleEnrollment")
    {
        return (Get-LanguageString "SettingDetails.appleEnrollmentTitle")
    }
    elseif($objTypeId -eq "PolicySets")
    {
        return (Get-LanguageString "SettingDetails.policySetsTitle")
    }    
    elseif($objTypeId -eq "AppConfiguration")
    {
        return (Get-LanguageString "SettingDetails.appConfiguration")
    }    
    elseif($objTypeId -eq "AppProtection")
    {
        return (Get-LanguageString "SettingDetails.appProtectionPolicy")
    } 
    elseif($objTypeId -eq "CustomAttributes")
    {
        return (Get-LanguageString "Titles.customAttributes")
    }   
    elseif($objTypeId -eq "ConditionalAccess")
    {
        return (Get-LanguageString "SecurityTemplate.conditionalAccess")
    }
    elseif($objTypeId -eq "EndpointAnalytics")
    {
        return (Get-LanguageString "SettingDetails.healthMonScopeBootPerf")
    }
    elseif($objTypeId -eq "EndpointSecurity")
    {
        return (Get-LanguageString "PolicyType.EndpointSecurityTemplate.default")
    }
    elseif($objTypeId -eq "EnrollmentRestrictions")
    {
        return (Get-LanguageString "Titles.enrollmentRestrictions")
    }
    elseif($objTypeId -eq "Scripts")
    {
        return (Get-LanguageString "Titles.scriptManagement")
    }
    elseif($objTypeId -eq "WinUpdatePolicies")
    {
        return (Get-LanguageString "Titles.windows10UpdateRings")
    }
    elseif($objTypeId -eq "WinFeatureUpdates")
    {
        return (Get-LanguageString "Titles.featureUpdateDeployments")
    }
    elseif($objTypeId -eq "WinQualityUpdates")
    {
        return (Get-LanguageString "Titles.windows10QualityUpdate")
    }    
    elseif($objTypeId -eq "WinDriverUpdatePolicies")
    {
        return (Get-LanguageString "Titles.windows10DriverUpdate")
    }
    elseif($objTypeId -eq "TenantAdmin")
    {
        return (Get-LanguageString "Titles.tenantAdmin")
    }
    elseif($objTypeId -eq "Azure")
    {
        return "Azure"
    }    
}

function Add-BasicDefaultValues
{
    param($obj, $objectType)

    Add-BasicPropertyValue (Get-LanguageString "SettingDetails.nameName") (Get-GraphObjectName $obj $objectType)
    Add-BasicPropertyValue (Get-LanguageString "SettingDetails.descriptionName") $obj.description

    $objInfo = Get-TranslationFiles $obj.'@OData.Type'
    $appType = Get-GraphAppType $obj

    if($objInfo)
    {
        $platformType = Get-LanguageString "Platform.$($objInfo.PlatformLanguageId)"
        $profileType = Get-LanguageString "ConfigurationTypes.$($objInfo.PolicyType)"

        if($platformType) { Add-BasicPropertyValue (Get-LanguageString "SettingDetails.platformSupported") $platformType }
        if($profileType) { Add-BasicPropertyValue (Get-LanguageString "TableHeaders.configurationType") $profileType }
    }

    if($appType)
    {
        $appTypeName = Get-LanguageString "AppType.$($appType.LanguageId)"
        if($appTypeName) { Add-BasicPropertyValue (Get-LanguageString "Inputs.installationSourceLabel") $appTypeName }
    }

    if($obj."@OData.Type" -eq "#microsoft.graph.deviceManagementIntent")
    {
        if(-not $script:baseLineTemplates)
        {
            $script:baseLineTemplates = (Invoke-GraphRequest -Url "/deviceManagement/templates").Value
        }

        $baseLineTemplate = $script:baseLineTemplates | Where Id -eq $obj.templateId
        if(-not $baseLineTemplate)
        {
            Write-Log "Could not find Baseline Template with Id $($obj.templateId)" 3            
        }
    
        $platformType = Get-LanguageString "Platform.$($baseLineTemplate.platformType)"

        if($platformType) { Add-BasicPropertyValue (Get-LanguageString "SettingDetails.platformSupported") $platformType }
        Add-BasicPropertyValue (Get-LanguageString "TableHeaders.Category") (Get-IntentCategory (?: ($baseLineTemplate.templateSubtype -eq "none") $baseLineTemplate.templateType $baseLineTemplate.templateSubtype))
        Add-BasicPropertyValue (Get-LanguageString "TableHeaders.policyType") $baseLineTemplate.displayName
    }

    if($obj.deviceManagementApplicabilityRuleDeviceMode)
    {
        # Future setting?
    }

    if($obj.deviceManagementApplicabilityRuleOsEdition)
    {
        $arrOSList = @()
        foreach($os in $obj.deviceManagementApplicabilityRuleOsEdition.osEditionTypes)
        {
            $arrOSList += Get-LanguageString "ApplicabilityRules.$os"
        }
        $script:applicabilityRules += [PSCustomObject]@{
            Rule = Get-LanguageString "ApplicabilityRules.$((?: ($obj.deviceManagementApplicabilityRuleOsEdition.ruleType -eq "exclude") "dontAssignIf" "assignIf"))"
            Property = Get-LanguageString "ApplicabilityRules.windows10OsEdition"
            Value = $arrOSList -join $script:objectSeparator
            Id = "deviceManagementApplicabilityRuleOsEdition"
            Category = Get-LanguageString "SettingDetails.applicabilityRules"
        }
    }
    if($obj.deviceManagementApplicabilityRuleOsVersion)
    {        
        $script:applicabilityRules += [PSCustomObject]@{
            Rule = Get-LanguageString "ApplicabilityRules.$((?: ($obj.deviceManagementApplicabilityRuleOsVersion.ruleType -eq "exclude") "dontAssignIf" "assignIf"))"
            Property = Get-LanguageString "ApplicabilityRules.windows10OsVersion"
            Value = "$($obj.deviceManagementApplicabilityRuleOsVersion.minOSVersion) $((Get-LanguageString "ApplicabilityRules.toText")) $($obj.deviceManagementApplicabilityRuleOsVersion.maxOSVersion)"
            Id = "deviceManagementApplicabilityRuleOsVersion"
            Category = Get-LanguageString "SettingDetails.applicabilityRules"
        }        
    }
}

function Add-CustomTable
{
    param($TableId, $Columns = @("Name", "Value"), $Values, [int]$Order = 100, $LanguageId = "")

    $script:customTables += [PSCustomObject]@{
        Id = $TableId
        Columns = $Columns
        Values = $Values
        LanguageId = $LanguageId
        Order = $Order
    }
}

function Add-BasicAdditionalValues
{
    param($obj, $objectType)

    if($obj.createdDateTime)
    {
        try
        {
            if($obj.createdDateTime -is [DateTime])
            {
                $tmpDate = $obj.createdDateTime
                if($tmpDate.Kind -eq "UTC")
                {
                    $tmpDate = $tmpDate.ToLocalTime()
                }
            }
            else
            {
                $tmpDate = ([DateTime]::Parse($obj.createdDateTime))
            }
            $tmpDateStr = "$($tmpDate.ToLongDateString()) $($tmpDate.ToLongTimeString())"
            
        }
        catch
        {
            Write-Log "Failed to parse date from $($obj.createdDateTime) (Object type: $($obj.createdDateTime.GetType().Name))" 2
            $tmpDateStr = $obj.createdDateTime
        }
        Add-BasicPropertyValue (Get-LanguageString "Inputs.createdDateTime") $tmpDateStr
    }

    if($obj.lastModifiedDateTime)
    {
        try
        {
            if($obj.lastModifiedDateTime -is [DateTime])
            {
                $tmpDate = $obj.lastModifiedDateTime
                if($tmpDate.Kind -eq "UTC")
                {
                    $tmpDate = $tmpDate.ToLocalTime()
                }
            }
            else
            {
                $tmpDate = ([DateTime]::Parse($obj.lastModifiedDateTime))
            }
            $tmpDateStr = "$($tmpDate.ToLongDateString()) $($tmpDate.ToLongTimeString())"
        }
        catch
        {
            Write-Log "Failed to parse date from $($obj.lastModifiedDateTime) (Object type: $($obj.lastModifiedDateTime.GetType().Name))" 2
            $tmpDateStr = $obj.lastModifiedDateTime
        }
        Add-BasicPropertyValue (Get-LanguageString "TableHeaders.lastModified") $tmpDateStr
    }
    elseif($obj.modifiedDateTime)
    {
        try
        {
            if($obj.modifiedDateTime -is [DateTime])
            {
                $tmpDate = $obj.modifiedDateTime
                if($tmpDate.Kind -eq "UTC")
                {
                    $tmpDate = $tmpDate.ToLocalTime()
                }
            }
            else
            {
                $tmpDate = ([DateTime]::Parse($obj.modifiedDateTime))
            }
            $tmpDateStr = "$($tmpDate.ToLongDateString()) $($tmpDate.ToLongTimeString())"
        }
        catch
        {
            Write-Log "Failed to parse date from $($obj.modifiedDateTime) (Object type: $($obj.modifiedDateTime.GetType().Name))" 2
            $tmpDateStr = $obj.modifiedDateTime
        }
        Add-BasicPropertyValue (Get-LanguageString "TableHeaders.lastModified") $tmpDateStr
    }

    if($obj.version)
    {
        Add-BasicPropertyValue (Get-LanguageString "SettingDetails.eDPPolicyAppsListVersionName") $obj.version
    }
    #Add-BasicPropertyValue (Get-LanguageString "TableHeaders.type") $obj.'@OData.Type'
}

function Add-BasicPropertyValue
{
    param($name, $value)

    $script:objectBasicInfo += [PSCustomObject]@{
        Name=$name
        Value=$value
        Category=(Get-LanguageString "SettingDetails.basics")
    }
}

function Add-ScopeTagStrings
{
    param($obj)

    $objScopeTags = @()
    if(($obj.PSObject.Properties | Where Name -eq "roleScopeTagIds"))
    {
        $scopeTagProperty = "roleScopeTagIds"
    }
    elseif(($obj.PSObject.Properties | Where Name -eq "roleScopeTags"))
    {
        $scopeTagProperty = "roleScopeTags"
    }
    else
    {
        return
    }
    
    if(($obj."$scopeTagProperty" | measure).Count -gt 0)
    {
        foreach($scopeTagId in $obj."$scopeTagProperty")
        {          
            $scopeTagName = $scopeTagId
            if($scopeTagId -eq "0")
            {
                $scopeTagName = (Get-LanguageString "SettingDetails.default")
            }
            elseif($script:scopeTags)
            {
                $scopeTagObj = $script:scopeTags | Where Id -eq $scopeTagId
                if($scopeTagObj.displayName)
                {
                    $scopeTagName = $scopeTagObj.displayName                
                }
            }
            $objScopeTags += $scopeTagName
        }
    }
    if($objScopeTags.Count -gt 0)
    {
        Add-BasicPropertyValue (Get-LanguageString "TableHeaders.scopeTags") ($objScopeTags -join $script:objectSeparator)
    }
}

function Add-ObjectScript
{
    param($Header, 
            $Caption, 
            $Script,
            [ValidateSet("Base64", "String")]
            [string]
            $ScriptType = "Base64")

    if($global:chkIncludeScripts.IsChecked -ne $true) { return }

    if($ScriptType -eq "Base64")
    {
        $scriptContent = Get-Base64ScriptContent $Script -RemoveSignature:$false
    }
    else
    {
        $scriptContent = $script
    }

    $RemoveSignature = $global:chkExcludeScriptSignature.IsChecked -eq $true
    if($RemoveSignature -eq $true)
    {
        $x = $scriptContent.IndexOf("# SIG # Begin signature block")
        if($x -gt 0)
        {
            $scriptContent = $scriptContent.SubString(0,$x)
            $scriptContent = $scriptContent + "# SIG # Begin signature block`nSignature data excluded..."
        }
    }    

    $script:objectScripts += [PSCustomObject]@{
        Header = $Header
        Caption = $Caption
        ScriptContent = $scriptContent
    }
}

function Get-ObjectPlatformFromType
{
    param($obj)

    $platform = $null

    $lowerAppType = $obj.'@OData.Type'.ToLower()
    if($lowerAppType.Contains("ios"))
    {
        $platform = "iOS"
    }
    elseif($lowerAppType.Contains("mac"))
    {
        $platform = "Mac"
    }
    elseif($lowerAppType.Contains("windowsphone"))
    {
        $platform = "WindowsPhone"
    }
    elseif($lowerAppType.Contains("windows") -or $lowerAppType.Contains("win32") -or $lowerAppType.Contains("mirosoftstore"))
    {
        $platform = "Windows10"
    }
    elseif($lowerAppType.Contains("androidForWork"))
    {
        $platform = "androidForWork"
    }
    elseif($lowerAppType.Contains("android"))
    {
        $platform = "Android"
    }

    $platform
}

#region Admin Templates

function Invoke-TranslateADMXObject
{
    param($obj, $objectType)

    Add-BasicDefaultValues $obj $objectType
    Add-BasicPropertyValue (Get-LanguageString "TableHeaders.configurationType") (Get-LanguageString "Titles.groupPolicy")
    #Add-BasicPropertyValue (Get-LanguageString "SettingDetails.platformSupported") $platformType
    Add-BasicAdditionalValues $obj $objectType

    $params = @{}
    ## Set language
    if($script:DocumentationLanguage)
    {
        $params.Add("AdditionalHeaders", @{"Accept-Language"=$script:DocumentationLanguage})
    }

    if(-not $script:admxCategories)
    {
        $script:admxCategories = (Invoke-GraphRequest "deviceManagement/groupPolicyCategories?`$expand=parent(`$select=id, displayName, isRoot),definitions(`$select=id, displayName, categoryPath, classType, policyType)&`$select=id, displayName, isRoot" -ODataMetadata "skip" @params).value
    }

    if(!$obj.definitionValues)
    {
        $definitionValues = (Invoke-GraphRequest "deviceManagement/groupPolicyConfigurations('$($obj.Id)')/definitionValues?`$expand=definition(`$select=id,classType,displayName,policyType,groupPolicyCategoryId)" -ODataMetadata "minimal").value
    }
    else
    {
        # Documenting exported json
        $definitionValues = $obj.definitionValues
    }

    $enabledStr = Get-LanguageString "Inputs.enabled"
    $disabledStr = Get-LanguageString "Inputs.disabled"

    $propertyStr = Get-LanguageString "ApplicabilityRules.GridLabel.property"
    $valueStr = Get-LanguageString "ApplicabilityRules.GridLabel.value"


    foreach($definitionValue in $definitionValues)
    {
        if(-not $definitionValue.definition -and $definitionValues.'definition@odata.bind')
        {
            $url = $definitionValue.'definition@odata.bind' -replace $global:graphURL, ("https://$((?? $global:MSALGraphEnvironment "graph.microsoft.com"))/beta")
            $definition = Invoke-GraphRequest -Url $url -ODataMetadata "minimal" @params
            if($definition)
            {
                $definitionValue | Add-Member -MemberType NoteProperty -Name "definition" -Value $definition
            }            
        }

        $categoryObj = $script:admxCategories | Where { $definitionValue.definition.id -in ($_.definitions.id) }
        $category = $script:admxCategories.definitions | Where { $definitionValue.definition.id -in ($_.id) }
        $settingPresentationValues = $null

        # Get presentation values for the current settings (with presentation object included)
        if($definitionValue.presentationValues -or $obj.'@ObjectFromFile' -eq $true) #$definitionValue.'definition@odata.bind')
        {
            $settingPresentationValues = (Invoke-GraphRequest -Url "$($definitionValue.'definition@odata.bind')/presentations"  -ODataMetadata "minimal").value
            
            $presentationValues = @()
            if($settingPresentationValues)
            {
                # Do this to make sure they are documented in the correct order
                foreach($settingPresentationValue in $settingPresentationValues)
                {
                    $tmpPresentationVal = $definitionValue.presentationValues | Where 'presentation@odata.bind' -Like "*$($settingPresentationValue.Id)*"
                    if($tmpPresentationVal)
                    {
                        $presentationValues += $tmpPresentationVal
                    }
                    else 
                    {
                        $presentationValues = @()
                        break
                    }
                }
            }

            if($presentationValues.Count -eq 0)
            {
                Write-Log "Could not find definition for definition id '$($definitionValue.id)'. Values might be documented in the wrong order!" 2
                $presentationValues = $definitionValue.presentationValues
            }
        }
        elseif($definitionValue.id)
        {
            $presentationValues = (Invoke-GraphRequest -Url "/deviceManagement/groupPolicyConfigurations/$($obj.id)/definitionValues/$($definitionValue.id)/presentationValues?`$expand=presentation"  -ODataMetadata "minimal" @params).value                
        }
        else
        {
            $presentationValues = $null    
        }

        $value = $null
        $rawValues = @()
        $values = @()
        $valuesWithLabel = @()
        $tableValue = @()

        foreach($presentationValue in $presentationValues)
        {
            if(-not $presentationValue.presentation -and $presentationValue.'presentation@odata.bind')
            {
                $presentation = Invoke-GraphRequest -Url $presentationValue.'presentation@odata.bind'
                if($presentation)
                {
                    $presentationValue | Add-Member -MemberType NoteProperty -Name "presentation" -Value $presentation
                }
            }
            
            $rawValue = $presentationValue.value
            $label = $presentationValue.presentation.label
            $value = $null
            if($presentationValue.presentation.'@odata.type' -eq '#microsoft.graph.groupPolicyPresentationDropdownList')
            {                
                $value = ($presentationValue.presentation.items | Where value -eq $rawValue).displayName
            }
            elseif($presentationValue.'@odata.type' -eq '#microsoft.graph.groupPolicyPresentationValueList')
            {
                $arrValues = @()
                foreach($tmpValue in $presentationValue.values)
                {
                    $arrValues += ($tmpValue.name + $script:propertySeparator + $tmpValue.value)
                }
                $value = $arrValues -join $script:objectSeparator
            }
            elseif($presentationValue.'@odata.type' -eq '#microsoft.graph.groupPolicyPresentationValueMultiText')
            {
                $value = $presentationValue.values -join $script:objectSeparator
            }
            else
            {
                #groupPolicyPresentationValueBoolean
                #groupPolicyPresentationValueDecimal
                #groupPolicyPresentationValueLongDecimal
                #groupPolicyPresentationValueText

                $value = $rawValue
            }
            $htFullValue = [ordered]@{}
            $htFullValue.Add($propertyStr,$label)
            $htFullValue.Add($valueStr,$value)

            $tableValue += [PSCustomObject]$htFullValue 

            $valuesWithLabel += "$label $value"
            $values += $value
            $rawValues += $rawValue
        }
        $status = (?: ($definitionValue.enabled -eq $true) $enabledStr $disabledStr)

        $combinedValue = $status 
        if($values) {
            $combinedValue += $script:objectSeparator + ($values -join $script:objectSeparator)
        }

        $combinedValueWithLabel = $status 
        if($valuesWithLabel) {
            $combinedValueWithLabel += $script:objectSeparator + ($valuesWithLabel -join $script:objectSeparator)
        }

        $script:objectSettingsData += New-Object PSObject -Property @{ 
            Name = $definitionValue.definition.displayName
            Description = $definitionValue.definition.explainText
            Status = $status
            Value = $values -join $script:objectSeparator
            CombinedValue = $combinedValue #($status + $script:objectSeparator + ($values -join $script:objectSeparator))
            ValueWithLabel = $valuesWithLabel -join $script:objectSeparator
            FullValueTable = $tableValue
            CombinedValueWithLabel = $combinedValueWithLabel #($status + $script:objectSeparator + ($valuesWithLabel -join $script:objectSeparator))
            RawValue = $rawValues -join $script:propertySeparator
            Class = $definitionValue.definition.classType
            DefinitionId = $definitionValue.definition.id
            Created = $definitionValue.createdDateTime
            Modified = $definitionValue.lastModifiedDateTime
            #Category = $categoryObj.displayName
            Category = $category.categoryPath
            CategoryPath = $category.categoryPath
            EntityKey = $definitionValue.definition.id # Required for Compare
        }        
    }

    $script:objectSettingsData = $script:objectSettingsData | Sort -Property CategoryPath
}


#endregion


#region Settings Catalog

function Invoke-TranslateSettingsObject
{
    param($obj, $objectType)

    $platformType = Get-LanguageString "Platform.$($obj.platforms)"

    Add-BasicDefaultValues $obj $objectType
    Add-BasicPropertyValue (Get-LanguageString "TableHeaders.configurationType") (Get-LanguageString "ConfigurationTypes.settingsCatalog")
    if($obj.templateReference.templateId)
    {
        Add-BasicPropertyValue (Get-LanguageString "TableHeaders.Category") (Get-IntentCategory $obj.templateReference.templateFamily)
        Add-BasicPropertyValue (Get-LanguageString "TableHeaders.policyType") $obj.templateReference.templateDisplayName

        #Add-BasicPropertyValue (Get-LanguageString "PolicyType.EndpointSecurityTemplate.default") $obj.templateReference.templateDisplayName
    }
    Add-BasicPropertyValue (Get-LanguageString "SettingDetails.platformSupported") $platformType
    Add-BasicAdditionalValues $obj $objectType
    
    $params = @{}
    ## Set language
    if($script:DocumentationLanguage)
    {
        $params.Add("AdditionalHeaders", @{"Accept-Language"=$script:DocumentationLanguage})
    }

    <#
    if($obj.templateReference.templateId)
    {
        $cfgSettings = (Invoke-GraphRequest "/deviceManagement/configurationPolicyTemplates('$($obj.templateReference.templateId)')/settingTemplates?`$expand=settingDefinitions&top=1000" -ODataMetadata "minimal" @params).Value
    }
    else
    {
        $cfgSettings = (Invoke-GraphRequest "/deviceManagement/configurationPolicies('$($obj.Id)')/settings?`$expand=settingDefinitions&top=1000" -ODataMetadata "minimal" @params).Value
    }
    #>
    $cfgSettings = (Invoke-GraphRequest "/deviceManagement/configurationPolicies('$($obj.Id)')/settings?`$expand=settingDefinitions&top=1000" -ODataMetadata "minimal" @params).Value

    if($obj.'@ObjectFromFile')
    {
        $cfgSettings = $obj.Settings
    }

    if(-not $global:cfgCategories -or -not ($global:cfgCategories | where { $_.settingUsage -eq "configuration" }))
    {
        $global:cfgCategories += (Invoke-GraphRequest "/deviceManagement/configurationCategories?`$filter=platforms has 'windows10' and technologies has 'mdm'" -ODataMetadata "minimal" @params).Value
    }

    if(-not $global:cachedCfgSettings) 
    {
        $global:cachedCfgSettings = @{}
    }

    $script:settingCatalogasCategories = @{}
    foreach($cfgSetting in $cfgSettings)
    {
        if($obj.'@ObjectFromFile' -and -not $cfgSetting.settingDefinitions) 
        {
            if($global:cachedCfgSettings.ContainsKey($cfgSetting.settingInstance.settingDefinitionId) -eq $false) 
            {
                $defObj = Invoke-GraphRequest "/deviceManagement/configurationSettings/$($cfgSetting.settingInstance.settingDefinitionId)"
                $global:cachedCfgSettings.Add($defObj.Id, $defObj)
            }
        }
        else
        {
            $defObj = $cfgSetting.settingDefinitions | Where id -eq $cfgSetting.settingInstance.settingDefinitionId
            if($global:cachedCfgSettings.ContainsKey($cfgSetting.settingInstance.settingDefinitionId) -eq $false) 
            {                
                $global:cachedCfgSettings.Add($defObj.Id, $defObj)
            }
        }
        #$defObj = $cfgSetting.settingDefinitions | Where { $_.id -eq $cfgSetting.settingInstance.settingDefinitionId -or $_.id -eq $cfgSettings.settingInstanceTemplate.settingDefinitionId }
        if(-not $defObj -or $script:settingCatalogasCategories.ContainsKey($defObj.categoryId)) { continue }

        $catObj = $global:cfgCategories | Where Id -eq $defObj.categoryId 
        $rootCatObj = $global:cfgCategories | Where Id -eq $catObj.rootCategoryId

        #$catSettings = Invoke-GraphRequest "/deviceManagement/configurationSettings?`$filter=categoryId eq '$($defObj.categoryId)' and applicability/platform has 'windows10' and applicability/technologies has 'mdm'" -ODataMetadata "minimal" @params

        $script:settingCatalogasCategories.Add($defObj.categoryId, (New-Object PSObject -Property @{ 
            Category=$catObj
            #Settings=$catSettings
            RootCategory=$rootCatObj
        }))
    }

    $script:curSettingsCatologPolicy = @()

    $cfgSettings | % { Add-SettingsSetting $_.settingInstance $_.settingDefinitions } | Out-Null

    #$script:objectSettingsData = $script:curSettingsCatologPolicy
    
    foreach($item in ($script:curSettingsCatologPolicy | Select @{l="CategoryID";e={$_.CategoryDefinition.Id}}, @{l="SubCategoryID";e={$_.SubCategoryDefinition.Id}} -Unique))
    {
        $script:objectSettingsData += ($script:curSettingsCatologPolicy | Where { $_.CategoryDefinition.Id -eq $item.CategoryID -and $_.SubCategoryDefinition.Id -eq $item.SubCategoryID })   
    }
    
    if($docProvider.PostSettingsCatalog)
    {
        & $docProvider.PostSettingsCatalog $obj $objectType $script:objectSettingsData
    }
}

function Add-SettingsSetting
{
    param($settingInstance, $settingsDefs, $ItemLevel, [switch]$SkippAdd)

    $defaultValue = $null
    $tableValue = $null
    $value = $null
    $rawValue = $null
    $rawJsonValue = $null
    $show = $true
    $children = @()
    $childSettings = @()

    $settingsDef = $settingsDefs | Where id -eq $settingInstance.settingDefinitionId
    if(-not $settingsDef -and $settingInstance.settingDefinitionId) 
    {
        if($global:cachedCfgSettings.ContainsKey($settingInstance.settingDefinitionId) -eq $false) 
        {
            $settingsDef = Invoke-GraphRequest "/deviceManagement/configurationSettings/$($settingInstance.settingDefinitionId)"
            $global:cachedCfgSettings.Add($settingInstance.settingDefinitionId, $settingsDef)
        }
        else
        {
            $settingsDef = $global:cachedCfgSettings[$settingInstance.settingDefinitionId]
        }        
    }
    $categoryDef = $global:cfgCategories | Where Id -eq $settingsDef.categoryId #$script:settingCatalogasCategories[$settingsDef.categoryId]

    if($settingsDef.categoryId -ne $categoryDef.rootCategoryId)
    {
        $objCategory = $global:cfgCategories | Where Id -eq $categoryDef.rootCategoryId
        #$objCategory = $script:settingCatalogasCategories[$categoryDef.RootCategory.Id]
        $subCategory = $categoryDef
    }
    else
    {
        $subCategory = $null
        $objCategory = $categoryDef
    }
    
    $settingName = ""
    $settingDescription = ""
    if($settingsDef.displayName) {
        $settingName = $settingsDef.displayName.Trim([Environment]::NewLine).Trim("`n")
    }

    if($settingsDef.description) {
        $settingDescription = $settingsDef.description.Trim([Environment]::NewLine).Trim("`n")
    }

    $settingInfo = [PSCustomObject]@{
        SettingId = $settingsDef.Id
        SettingKey = ""
        SettingName = $settingsDef.Name
        Name = $settingName
        Description = $settingDescription
        CategoryId = $objCategory.id
        Category=$objCategory.displayName
        CategoryDefinition=$objCategory
        SubCategory=$subCategory.displayName
        SubCategoryDefinition=$subCategory       
        Value = $null
        RawValue = $null
        RawJsonValue=$null
        TableValue = $null
        DefaultValue = $null
        Level = $ItemLevel
        Parent = $null
        Show = $show
        Type = $settingInstance.'@odata.type'
        PropertyIndex = 0
        RowIndex = 0
        ChildSettings = @() #($childSettings | Sort DisplayName)
    }

    $script:curSettingsCatologPolicy += $settingInfo

    if($settingInstance.'@odata.type' -eq '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance')
    { # Drop down and select one value
            
        $rawValue = $settingInstance.choiceSettingValue.value
        $value = ($settingsDef.Options | Where itemId -eq $rawValue).displayName

        if($settingsDef.defaultOptionId)
        {
            $defaultValue = ($settingsDef.Options | Where itemId -eq $settingsDef.defaultOptionId).displayName
        }

        foreach($childSetting in $settingInstance.choiceSettingValue.children)
        {
            $tmpSetting = Add-SettingsSetting $childSetting $settingsDefs ($ItemLevel + 1) -SkippAdd #-SkippAdd:$SkippAdd
            if($tmpSetting)
            {
                $tmpSetting.Parent = $settingInfo
                $settingInfo.ChildSettings += $tmpSetting
            }
        }
    }
    elseif($settingInstance.'@odata.type' -eq '#microsoft.graph.deviceManagementConfigurationSimpleSettingInstance')
    { # Simple value eg a string
        $value = $settingInstance.simpleSettingValue.value 
        $rawValue = $value
        if($settingsDef.defaultValue.value)
        {
            $defaultValue = $settingsDef.defaultValue.value
        }
    }
    elseif($settingInstance.'@odata.type' -eq '#microsoft.graph.deviceManagementConfigurationChoiceSettingCollectionInstance')
    { # Drop down and select one or more values
        $itemValues = @()
        $itemRawValues = @()
        $tableValue = [ordered]@{}

        foreach($colObj in $settingInstance.choiceSettingCollectionValue)
        {
            $itemRawValues += $colObj.value
            $itemValues += ($settingsDef.Options | Where itemId -eq $colObj.Value).displayName
        }

        $value = $itemValues -join $script:propertySeparator
        $rawValue = $itemRawValues -join $script:propertySeparator
        $rawJsonValue = $settingInstance.choiceSettingCollectionValue | ConvertTo-Json -Depth 50 -Compress

        if($settingsDef.defaultOptionId)
        {
            $defaultValue = ($settingsDef.Options | Where itemId -eq $settingsDef.defaultOptionId).displayName
        }                
    }
    elseif($settingInstance.'@odata.type' -eq '#microsoft.graph.deviceManagementConfigurationGroupSettingCollectionInstance')
    { # Multiple settings under one group. Group will not be displayed

        $settingInfo.Show = $false
        $index=1
        foreach($groupSettingCollection in $settingInstance.groupSettingCollectionValue)
        {
            $childSettingsArr = @()
            # Not sure if this is the best way but it looks better for tested policies
            if($script:currentObject.templateReference.templateId -and $settingsDefs)
            {
                $childIDs = $settingsDefs.id # Endpoint Security objects
            }
            else
            {
                $childIDs = $settingsDef.childIds # Setings Catalog and from file documentation
            }
            #foreach($childId in $settingsDefs.id) #$settingsDef.childIds)
            foreach($childId in $childIDs)            
            {            
                $childSetting = ($groupSettingCollection.children | Where settingDefinitionId -eq $childId)
                if(-not $childSetting) { continue } #Not configured
                $tmpSetting = Add-SettingsSetting $childSetting $settingsDefs ($ItemLevel + 1) -SkippAdd #-SkippAdd:$SkippAdd
                if($tmpSetting)
                {
                    $tmpSetting.Parent = $childSettings
                    $tmpSetting.RowIndex = $index
                    $childSettings += $tmpSetting
                    $childSettingsArr += $tmpSetting
                    if($settingsDef.childIds.Count -gt 1)
                    {
                        $tmpSetting.PropertyIndex = $childSettingsArr.Count
                    }
                }
                $rowIndex++
            }

            $settingInfo.ChildSettings += [PSCustomObject]@{
                Id=$index++
                Type=$groupSettingCollection.'@odata.type'
                Settings=$childSettingsArr
            }
        }

        $rawJsonValue = $settingInstance.groupSettingCollectionValue | ConvertTo-Json -Depth 50 -Compress
    }
    elseif($settingInstance.'@odata.type' -eq '#microsoft.graph.deviceManagementConfigurationSimpleSettingCollectionInstance')
    { # Group of simple values
        $itemValues = @()
        foreach($colObj in $settingInstance.simpleSettingCollectionValue)
        {
            $itemValues += $colObj.value            
        }

        if($settingsDef.defaultValue.value)
        {
            $defaultValue = $settingsDef.defaultValue.value
        }

        $value = $itemValues -join $script:propertySeparator
        $rawValue = $itemValues -join $script:propertySeparator
        $rawJsonValue = $settingInstance.simpleSettingCollectionValue | ConvertTo-Json -Depth 50 -Compress
    }
    elseif($settingInstance.'@odata.type' -eq '#microsoft.graph.deviceManagementConfigurationGroupSettingInstance')
    {  # This will skip adding the group itself as enabled...
       # It will only add information about the children

        $show = $false
        foreach($groupSettingValue in $settingInstance.groupSettingValue)
        {
            #$children += $groupSettingValue #.children
            foreach($childSetting in $groupSettingValue.children)
            {
                $tmpSetting = Add-SettingsSetting $childSetting $settingsDefs ($ItemLevel + 1) -SkippAdd #-SkippAdd:$SkippAdd
                if($tmpSetting)
                {
                    $tmpSetting.Parent = $settingInfo
                    $settingInfo.ChildSettings += $tmpSetting
                }
            }            
        }
        $rawJsonValue = $settingInstance.groupSettingValue | ConvertTo-Json -Depth 50 -Compress
    }
    else
    {
        Write-Log "Unhandled object type: $($settingInstance.'@odata.type')" 2
        return
    }

    if(!$rawJsonValue -and $rawValue)
    {
        $rawJsonValue = $rawValue | ConvertTo-Json -Depth 50 -Compress
    }

    $settingInfo.Value = $value
    $settingInfo.RawValue = $rawValue
    $settingInfo.RawJsonValue=$rawJsonValue
    $settingInfo.DefaultValue = $defaultValue

    $settingInfo
}

#endregion

#region Compliance V2 Policies - Based on Settings Catalog

function Invoke-TranslateComplianceV2Object
{
    param($obj, $objectType)

    $platformType = Get-LanguageString "Platform.$($obj.platforms)"

    Add-BasicDefaultValues $obj $objectType
    Add-BasicPropertyValue (Get-LanguageString "TableHeaders.configurationType") (Get-LanguageString "ConfigurationTypes.settingsCatalog")
    Add-BasicPropertyValue (Get-LanguageString "SettingDetails.platformSupported") $platformType
    Add-BasicAdditionalValues $obj $objectType
    
    $params = @{}
    ## Set language
    if($script:DocumentationLanguage)
    {
        $params.Add("AdditionalHeaders", @{"Accept-Language"=$script:DocumentationLanguage})
    }

    $cfgSettings = (Invoke-GraphRequest "/deviceManagement/compliancePolicies('$($obj.Id)')/settings?`$expand=settingDefinitions&top=1000" -ODataMetadata "minimal" @params).Value

    if($obj.'@ObjectFromFile')
    {
        $cfgSettings = $obj.Settings
    }

    if(-not $global:cfgCategories -or -not ($global:cfgCategories | where { $_.settingUsage -eq "compliance" }))
    {
        $global:cfgCategories += (Invoke-GraphRequest "/deviceManagement/complianceCategories?`$templateCategory=True&`$filter=platforms has 'linux' and technologies has 'linuxMdm'" -ODataMetadata "minimal" @params).Value
    }

    if(-not $global:cachedCfgSettings) 
    {
        $global:cachedCfgSettings = @{}
    }

    $script:settingCatalogasCategories = @{}
    foreach($cfgSetting in $cfgSettings)
    {
        if($obj.'@ObjectFromFile' -and -not $cfgSetting.settingDefinitions) 
        {
            if($global:cachedCfgSettings.ContainsKey($cfgSetting.settingInstance.settingDefinitionId) -eq $false) 
            {
                $defObj = Invoke-GraphRequest "/deviceManagement/configurationSettings/$($cfgSetting.settingInstance.settingDefinitionId)"
                $global:cachedCfgSettings.Add($defObj.Id, $defObj)
            }
        }
        else
        {
            $defObj = $cfgSetting.settingDefinitions | Where id -eq $cfgSetting.settingInstance.settingDefinitionId
            if($global:cachedCfgSettings.ContainsKey($cfgSetting.settingInstance.settingDefinitionId) -eq $false) 
            {                
                $global:cachedCfgSettings.Add($defObj.Id, $defObj)
            }
        }
        if(-not $defObj -or $script:settingCatalogasCategories.ContainsKey($defObj.categoryId)) { continue }

        $catObj = $global:cfgCategories | Where Id -eq $defObj.categoryId 
        $rootCatObj = $global:cfgCategories | Where Id -eq $catObj.rootCategoryId

        $script:settingCatalogasCategories.Add($defObj.categoryId, (New-Object PSObject -Property @{ 
            Category=$catObj
            RootCategory=$rootCatObj
        }))
    }

    $script:curSettingsCatologPolicy = @()

    $cfgSettings | % { Add-SettingsSetting $_.settingInstance $_.settingDefinitions } | Out-Null

    foreach($item in ($script:curSettingsCatologPolicy | Select @{l="CategoryID";e={$_.CategoryDefinition.Id}}, @{l="SubCategoryID";e={$_.SubCategoryDefinition.Id}} -Unique))
    {
        $script:objectSettingsData += ($script:curSettingsCatologPolicy | Where { $_.CategoryDefinition.Id -eq $item.CategoryID -and $_.SubCategoryDefinition.Id -eq $item.SubCategoryID })   
    }
}

#endregion

#region Intent Objects (Endpoint Security)

function Get-IntentCategory
{
    param($templateType)

    if(-not $templateType)
    {
        Write-Log "Get-IntentCategory called with empty Category" 2
        return
    }

    if($templateType.StartsWith("endpointSecurity"))
    {
        $templateType = $templateType.Substring(16)
    }

    if($templateType -eq "accountProtection")
    {
        return (Get-LanguageString "SecurityTemplate.accountProtection")
    }
    elseif($templateType -eq "antivirus")
    {
        return (Get-LanguageString "SecurityTemplate.antivirus")
    }
    elseif($templateType -eq "diskEncryption")
    {
        return (Get-LanguageString "SecurityTemplate.diskEncryption")
    }
    elseif($templateType -eq "endpointDetectionReponse")
    {
        return (Get-LanguageString "SecurityTemplate.eDR")
    }    
    elseif($templateType -eq "attackSurfaceReduction")
    {
        return (Get-LanguageString "SecurityTemplate.aSR")
    }
    elseif($templateType -eq "attackSurfaceReduction")
    {
        return (Get-LanguageString "SecurityTemplate.aSR")
    }
    elseif($templateType -eq "firewall")
    {
        return (Get-LanguageString "SecurityTemplate.firewall")
    }
    elseif($templateType -eq "securityBaseline" -or 
        $templateType -eq "baseline" -or
        $templateType -eq "advancedThreatProtectionSecurityBaseline" -or
        $templateType -eq "microsoftEdgeSecurityBaseline")
    {
        return (Get-LanguageString "Titles.securityBaselines")
    }
    else
    {
        Write-Log "Could not translate templateSubtype $templateType"
        return $templateType
    }
}

function Invoke-TranslateIntentObject
{
    param($obj, $objectType)

    $params = @{}
    if($script:DocumentationLanguage)
    {
        $params.Add("AdditionalHeaders", @{"Accept-Language"=$script:DocumentationLanguage})
    }

    Add-BasicDefaultValues $obj $objectType
    Add-BasicAdditionalValues $obj $objectType

    if($global:intentCategories -is [HashTable])
    {
        $categories = $global:intentCategories[$obj.TemplateId]
    }
    else
    {
        $global:intentCategories = @{}
        $global:catRecommendedSettings = @{}
    }

    if($global:intentCategoryDefs -isnot [HashTable])
    {
        $global:intentCategoryDefs = @{}
    }    

    if(-not $categories)
    {
        $categories = (Invoke-GraphRequest "/deviceManagement/templates/$($obj.TemplateId)/categories?`$expand=settingDefinitions" -ODataMetadata "minimal" @params).Value
        $global:intentCategories.Add($obj.TemplateId, $categories)
    }
    
    $script:objectSettings = @()

    foreach($category in ($categories | Sort -Property displayName))
    {
        # Get settings for the category. This will put them in the correct order...
        if($obj.'@ObjectFromFile' -ne $true)
        {
            $settings = (Invoke-GraphRequest "/deviceManagement/intents/$($obj.Id)/categories/$($category.Id)/settings?`$expand=Microsoft.Graph.DeviceManagementComplexSettingInstance/Value" -ODataMetadata "minimal" @params).Value
        }
        else 
        {
            $settings = $obj.settings
        }

        if($global:catRecommendedSettings.ContainsKey($category.Id))
        {
            $catRecommendedSettingsObj = $global:catRecommendedSettings[$obj.TemplateId]
        }
        else
        {
            # Get setting defenitions for the category
            #$catDefObjs = (Invoke-GraphRequest "/devicemanagement/templates/$($obj.TemplateId)/categories/$($category.Id)/settingDefinitions" -ODataMetadata "minimal").Value
            #$global:intentCategoryDefs.Add($category.Id,$catDefObjs)
            # Get recommended settings for the category
            $catRecommendedSettingsObj = (Invoke-GraphRequest "/deviceManagement/templates/$($obj.TemplateId)/categories/$($category.Id)/RecommendedSettings?" -ODataMetadata "minimal" @params).Value
            $global:catRecommendedSettings.Add($category.Id, $catRecommendedSettingsObj)
        }

        $catDefIds = ($category.settingDefinitions | Select Id).Id        

        foreach($settingObj in $settings) #($settings | Where { $_.definitionId -in $catDefIds }))
        {
            Get-IntentSettingInfo $settingObj $category $settingObj.definitionId $settings
        }
    }
    
    foreach($objSetting in ($script:objectSettings | Where { $_.ParentId -eq $null -and ($_.Dependencies | measure).Count -eq 0 }))
    {
        Add-IntentSettingObjectToList $objSetting
    }
}

function Add-IntentSettingObjectToList
{
    param($objSetting)

    if(($script:objectSettingsData | Where Id -eq $objSetting.Id)) { return }

    $passConstraint = $true
    $hasConstraint = $false
    foreach($dependencyObj in $objSetting.SettingDefinition.dependencies)
    {
        $dependencyItemObj = ($script:objectSettings | Where { $_.SettingDefinition.Id -eq $dependencyObj.definitionId })
        if($dependencyObj.constraints.Count -gt 0)
        {                    
            $hasConstraint = $true
            foreach($constraint in $dependencyObj.constraints)
            {
                if($constraint.'@odata.type' -eq "#microsoft.graph.deviceManagementSettingBooleanConstraint")
                {
                    if($dependencyItemObj.RawValue -eq $null -and $constraint.value -eq $false -or
                        ($dependencyItemObj.RawValue -and $dependencyItemObj.RawValue.ToString() -ne $constraint.value.ToString()))
                    {
                        $passConstraint = $false
                        break
                    }
                }
                elseif($constraint.'@odata.type' -eq "#microsoft.graph.deviceManagementEnumConstraint")
                {
                    if(-not ($constraint.values | Where Value -eq $dependencyItemObj.RawValue))
                    {
                        $passConstraint = $false
                        break
                    }
                }
                elseif($constraint.'@odata.type' -eq "#microsoft.graph.deviceManagementSettingIntegerConstraint")
                {
                    if($dependencyItemObj.RawValue -ge $constraint.minimumValue -and $dependencyItemObj.RawValue -le $constraint.maximumValue)
                    {
                        $passConstraint = $false
                        break
                    }
                }
            }
        }
        else
        {
            $passConstraint = ($dependencyItemObj.RawValue -ne $null -and $dependencyItemObj.RawValue.ToString() -ne "NotConfigured" -and $dependencyItemObj.RawValue.ToString() -ne "False")
        }
        if(-not $passConstraint) { break ]}
    }

    if(-not $passConstraint)
    {
        return
    }
    
    if($hasConstraint)
    {
        $objSetting.Level = $objSetting.Level + 1
    }

    $recommendedSetting = $global:catRecommendedSettings[$objSetting.CategoryObject.Id] | Where definitionId -eq  $objSetting.SettingId

    if($recommendedSetting.valueJson -and ($objSetting.ValueSet -eq $false -or $recommendedSetting.valueJson -ne ($objSetting.RawValue | ConvertTo-Json -Compress))) {
        $objSetting | Add-Member Noteproperty -Name "RecommendedValue" -Value ($recommendedSetting.valueJson | ConvertFrom-Json) -Force
    }    

    $script:objectSettingsData += $objSetting

    if($objSetting.ValueSet -eq $false) { return }

    foreach($depObj in ($script:objectSettings | Where { $_.Dependencies.definitionId -eq $objSetting.SettingDefinition.Id }))
    {
        Add-IntentSettingObjectToList $depObj
    }

    foreach($depObj in ($script:objectSettings | Where { $_.ParentId -eq $objSetting.Id -and ($_.Dependencies | measure).Count -eq 0} ))
    {
        Add-IntentSettingObjectToList $depObj    
    }
}

function Get-IntentSettingInfo
{
    param($valueObj, $category, $defId, $allSettings, [switch]$SkipConvertValue, [switch]$PassThru, $parentDef = $null)

    $defObj = $category.settingDefinitions | Where id -eq $defId    

    if(-not $defObj) { return } # Should never happen!

    $itemValue = $null
    $itemFullValue = $null

    if($SkipConvertValue -ne $true)
    {
        $rawValue = $valueObj.valueJson | ConvertFrom-Json
    }
    else
    {
        $rawValue = $valueObj
    }

    $valueSet = Get-IsIntentObjectConfigured $rawValue

    if($valueSet -eq $false)
    {
        ; # Skip child settings
    }
    elseif($valueObj.'@odata.type' -eq '#microsoft.graph.deviceManagementCollectionSettingInstance' -or
            $defObj.'@odata.type' -eq '#microsoft.graph.deviceManagementComplexSettingDefinition' -or
            $defObj."valueType" -eq  "collection")
    {
        $valueArr = @()
        
        if($defObj.elementDefinitionId)
        {
            # Get the element defenition of the items in the collection
            $elementDefObj = $category.settingDefinitions | Where id -eq $defObj.elementDefinitionId
        }
        else
        {
            $elementDefObj = $defObj
        }

        if($elementDefObj.propertyDefinitionIds)
        {
            # Elements are Complex and based of one or more definitions
            $itemFullValue = @()
            foreach($tmpValue in $rawValue)
            {
                $htFullPropInfo = [ordered]@{}
                foreach($tmpPropDefId in $propDefObj.propertyDefinitionIds)
                {
                    $tmpDef = $category.settingDefinitions | Where id -eq $tmpPropDefId
                    $tmpProp = $tmpPropDefId.Split('_')[-1]
                    $htFullPropInfo.Add((?? $tmpDef.displayName $tmpProp), $tmpPropValue.$tmpProp)
                }                

                $arrValue = ""
                foreach($propertyDefinitionId in $elementDefObj.propertyDefinitionIds)
                {
                    $propDefObj = $category.settingDefinitions | Where id -eq $propertyDefinitionId
                    if($propDefObj.elementDefinitionId)
                    {
                        # The collection element can be based of multiple definitions with specific element types
                        $propDefObj = $category.settingDefinitions | Where id -eq $propDefObj.elementDefinitionId
                    }
                    if($arrValue) { $arrValue = ($arrValue + $script:propertySeparator)}
                    $propName = $propertyDefinitionId.Split('_')[-1]
                    $propValue = @()
                    foreach($childTmpVales in $tmpValue.$propName)
                    {
                        $propValue += Get-IntentObjectValue $propDefObj $childTmpVales
                    }

                    $htFullPropInfo.Add((?? $propDefObj.displayName $propName), $tmpValue.$propName)                    
<#
                    $propFullValue = @()
                    foreach($tmpPropValue in $propValue)
                    {
                        $htFullPropInfo = [ordered]@{}
                        foreach($tmpPropDefId in $propDefObj.propertyDefinitionIds)
                        {
                            $tmpDef = $category.settingDefinitions | Where id -eq $tmpPropDefId
                            $tmpProp = $tmpPropDefId.Split('_')[-1]
                            $htFullPropInfo.Add((?? $tmpDef.displayName $tmpProp), $tmpPropValue.$tmpProp)
                        }
                        $propFullValue += [PSCustomObject]$htFullPropInfo
                    }
#>
                    $arrValue = ($arrValue + ($propValue -join $script:propertySeparator))
                }
                $itemFullValue += [PSCustomObject]$htFullPropInfo
                $valueArr += $arrValue
            }                        
        }
        elseif($rawValue)
        {
            # Elements are Strings, Integers etc.
            foreach($tmpValue in $rawValue)
            {
                $valueArr += (Get-IntentObjectValue $elementDefObj $tmpValue)
            }
        }

        if($valueArr.Count -gt 0)
        {
            $itemValue = $valueArr -join $script:objectSeparator # Or `n or?
        }
        $valueSet = $valueArr.Count -gt 0
    }
    elseif($valueObj.'@odata.type' -eq '#microsoft.graph.deviceManagementAbstractComplexSettingInstance' -or
        $defObj.'@odata.type' -eq '#microsoft.graph.deviceManagementAbstractComplexSettingDefinition')
    {
        $tmpDef = $category.settingDefinitions | Where { $_.id -eq $rawValue.implementationId -or $_.id -eq $rawValue.'$implementationId' }
        if($tmpDef)
        {
            $itemValue = $tmpDef.displayName
        }
        else
        {
            $valueSet = $false
        }
    }
    else
    {
        $itemValue = Get-IntentObjectValue $defObj $rawValue
        if(-not $itemValue) 
        {
            $valueSet = $false
        }
    }

    if($valueSet -eq $false)
    {
        $itemValue = Get-LanguageString "SettingDetails.notConfigured"
    }
    elseif(-not $itemValue)
    {
        $itemValue = $rawValue
    }

    $curObjectInfo = New-Object PSObject -Property @{ 
        Name=$defObj.displayName
        Description=$defObj.description
        Category=$category.displayName
        CategoryDescription=$category.description # Will not have a description
        CategoryObject=$category
        Value=$itemValue
        FullValueTable=$itemFullValue
        RawValue=$rawValue
        SettingDefinition = $defObj
        Dependencies = $defObj.dependencies
        ValueSet = $valueSet
        Id=[Guid]::NewGuid() #(([Guid]::NewGuid()).Guid)
        ParentId = $null
        SettingId = $defObj.Id # ToDo: Must have parent Id as well e.g. Firewall settings in Win10 baseline
        ParentSettingId = $parentDef.Id
        Level = 0
    }

    $script:objectSettings += $curObjectInfo    

    if($valueSet -eq $false)
    {
        ; # Skip children if value is not set...
    }
    elseif($valueObj.'@odata.type' -eq '#microsoft.graph.deviceManagementComplexSettingInstance' -or 
        $defObj.'@odata.type' -eq '#microsoft.graph.deviceManagementComplexSettingDefinition')
    {        
        if($valueObj.Value)
        {
            $isValueSet = $false
            $sortByDefinitionIds = $true # !!!
            if($sortByDefinitionIds -eq $true -and $defObj.propertyDefinitionIds)
            {
                foreach($childDefId in $defObj.propertyDefinitionIds)
                {
                    $childSetting = $valueObj.Value | Where DefinitionId -eq $childDefId
                    if($childSetting)
                    {
                        $objValueInfo = Get-IntentSettingInfo $childSetting $category $childSetting.definitionId $allSettings -PassThru -parentDef $defObj
                        $objValueInfo.ParentId = $curObjectInfo.Id
                        if($objValueInfo.RawValue -is [Boolean] -and $objValueInfo.RawValue -eq $true)
                        {
                            $isValueSet = $true
                        }
                        elseif($objValueInfo.RawValue -is [String] -and -not [String]::IsNullOrEmpty($objValueInfo.RawValue) -and $objValueInfo.RawValue -ne "notConfigured" -and -not [String]::IsNullOrEmpty($objValueInfo.Value))
                        {
                            $isValueSet = $true
                        }
                        elseif($objValueInfo.RawValue -isnot [Boolean] -and $objValueInfo.RawValue -isnot [String])
                        {
                            $isValueSet = $true
                        }
                    }
                }
            }
            else
            {
                foreach($childSetting in $valueObj.Value)
                {
                    $objValueInfo = Get-IntentSettingInfo $childSetting $category $childSetting.definitionId $allSettings -PassThru -parentDef $defObj
                    $objValueInfo.ParentId = $curObjectInfo.Id
                }
            }
        }
        elseif($rawValue -and $defObj.propertyDefinitionIds)
        {
            $isValueSet = $false
            $isDefault = $true
            foreach($childDefId in $defObj.propertyDefinitionIds)
            {
                $propName = $childDefId.Split('_')[-1]                
                $objValueInfo = Get-IntentSettingInfo $rawValue.$propName $category $childDefId $allSettings -SkipConvertValue -PassThru -parentDef $defObj
                if($objValueInfo.ValueSet -eq $true) { $isValueSet = $true }
                
                if($objValueInfo.SettingDefinition.constraints -and  
                    $objValueInfo.SettingDefinition.constraints[0].'@odata.type' -eq "#microsoft.graph.deviceManagementEnumConstraint" -and 
                    ($objValueInfo.SettingDefinition.constraints[0].values | measure).Count -gt 0)
                {
                    if($objValueInfo.SettingDefinition.constraints[0].values[0].value -ne $rawValue.$propName)
                    {
                        $isDefault = $false
                    }
                }
                elseif($objValueInfo.SettingDefinition.valueType -eq "string")
                {
                    if($null -ne $rawValue.$propName)
                    {
                        $isDefault = $false
                    }
                }
                elseif($objValueInfo.SettingDefinition.valueType -eq "boolean")
                {
                    if($false -ne $rawValue.$propName)
                    {
                        $isDefault = $false
                    }
                }                
                $objValueInfo.ParentId = $curObjectInfo.Id                
            }
            if($isDefault)
            {
                # All child items are using default settings so set value not set
                # ToDo: Verify ALL cases...don't like this but it looks like this is how it's done
                $isValueSet = $false
            }
        }
        else
        {
            $isValueSet = $false
        }

        $curObjectInfo.Value = ?: $isValueSet "Configure" (Get-LanguageString "SettingDetails.notConfigured")
        $curObjectInfo.ValueSet = $isValueSet
        $curObjectInfo.FullValueTable = $null
    }
    elseif(($valueObj.'@odata.type' -eq '#microsoft.graph.deviceManagementAbstractComplexSettingInstance' -or 
            $defObj.'@odata.type' -eq '#microsoft.graph.deviceManagementAbstractComplexSettingDefinition') -and
            $rawValue -and $tmpDef)
    {
        foreach($childDefId in $tmpDef.propertyDefinitionIds)
        {
            $propName = $childDefId.Split('_')[-1]

            $objValueInfo = Get-IntentSettingInfo $rawValue.$propName $category $childDefId $allSettings -SkipConvertValue -PassThru -parentDef $defObj
            $objValueInfo.ParentId = $curObjectInfo.Id
        }
    }

    if($PassThru)
    {
        $curObjectInfo
    }
}

function Get-IntentObjectValue
{
    param($defObj, $rawValue)

    $itemValue = $null

    if($defObj.constraints.'@odata.type' -eq "#microsoft.graph.deviceManagementEnumConstraint")
    {
        $tmpOption = $defObj.constraints.Values  | Where value -eq $rawValue
        if(-not $tmpOption -and $rawValue -eq $null)
        {
            # This is NOT ok. There is no defaultValue of the setting definitions
            # Ex when this is used:
            # deviceConfiguration--windows10EndpointProtectionConfiguration_defenderScanDirection
            $tmpOption =  $defObj.constraints.Values[0]
        }
        $itemValue = $tmpOption.displayName
    }
    elseif($defObj.valueType -eq "boolean")
    {
        if($rawValue -eq "True")
        {
            $itemValue = Get-LanguageString "SettingDetails.yes"
        }
    }
    else
    {
        $itemValue = $rawValue
    }

    $itemValue
}

function Get-IsIntentObjectConfigured
{
    param($obj)

    # Custom checks if needed

    return $true
}

#endregion

#region Profile Types e.g. DeviceConfiguration Profiles, Compliance Policies etc
function Invoke-TranslateProfileObject
{
    param($obj, $objectType)

    $objInfo = Get-TranslationFiles $obj.'@OData.Type'
    
    if(-not $objInfo)
    {        
        return $false
    }
    elseif(($objInfo | measure).Count -gt 1)
    {
        Write-Log "Multiple category types returned for $($obj.'@OData.Type'). Aborting..." 3
        return $false
    }

    Add-BasicDefaultValues $obj $objectType
    Add-BasicAdditionalValues $obj $objectType

    $allFiles = @()
    if($objInfo.Categories)
    {
        foreach($objCategory in ($objInfo.Categories))
        {
            $file = ($global:AppRootFolder + "\Documentation\ObjectInfo\$($objCategory)_$($objInfo.PolicyType).json")
            $fi = [IO.FileInfo]$file
            if($fi.Exists -eq $false)
            {
                Write-Log "Category file '$file' not found"
                continue
            }
            $allFiles += $fi 
        }
    }
    else
    {
        # Should only be one file. Compliance policies might have more
        $files = [IO.Directory]::EnumerateFiles($global:AppRootFolder + "\Documentation\ObjectInfo", "*_$($objInfo.PolicyType).json")
        if(($files | measure).Count -eq 0)
        {
            Write-Log "No category files returned for $($objInfo.PolicyType)" 2
        }
        elseif(($files | measure).Count -gt 1)
        {
            Write-Log "Multiple category files returned for $($objInfo.PolicyType)" 2
        }

        foreach($file in $files)
        {            
            $fi = [IO.FileInfo]$file
            $allFiles += $fi 
        }
    }

    Add-CustomProfileProperties $obj

    foreach($fi in $allFiles)
    {
        try 
        {
            $categoryObj = (Get-Content $fi.FullName -Encoding UTF8) | ConvertFrom-Json 
            $script:CurrentSubCategory = ""
            
            if($docProvider.TranslateSectionFile)
            {
                $retObj = & $docProvider.TranslateSectionFile $obj $objectType $fi $categoryObj
                if($retObj -is [Boolean] -and $retObj -eq $true)
                {
                    # Handled by custom function
                    continue
                }
            }            
            Invoke-TranslateSection $obj $categoryObj."$($fi.BaseName)" $objInfo            
        }
        catch 
        {
            Write-LogError "Failed to translate file $($fi.Name)" $_.Exception    
        }
    }

    return $true
}

function Get-LanguageString
{
    param($string, $defaultValue = $null, [switch]$IgnoreMissing)

    if(-not $script:languageStrings)
    {
        $lng = ?? $script:DocumentationLanguage "en"
        $fileContent = Get-Content ($global:AppRootFolder + "\Documentation\Strings-$($lng).json") -Encoding UTF8
        $script:languageStrings =  $fileContent | ConvertFrom-Json
    }

    if(!$string) { return }

    $arrParts = $string.Split('.')

    if([String]::IsNullOrEmpty($arrParts[-1])) { return }

    $obj = $script:languageStrings
    foreach($part in $arrParts.Split('.'))
    {
        if($obj.$part -or $part -eq "Empty")
        {
            $obj = $obj.$part
        }
        else
        {
            if($part -and $IgnoreMissing -ne $true)
            {
                Write-Host "Could not find string $string. Part '$part' was not found"
            }
            return $defaultValue
        }
    }
    return $obj.Trim("`n")
}

function Get-TranslationFiles
{
    param($type)

    if(-not $script:ObjectPolicyTypes)
    {
        $script:ObjectPolicyTypes = Get-Content ($global:AppRootFolder + "\Documentation\ObjectCategories.json") | ConvertFrom-Json    
    }

    return $script:ObjectPolicyTypes | Where ObjectType -eq $type
}

function Get-Category
{
    param($categoryId)

    if($categoryId -is [String])
    {
        return Get-LanguageString (?: $categoryId.Contains(".") $categoryId "Category.$($categoryId)")
    }

    if(-not $script:Categories)
    {
        $script:CategoryIds = Get-Content ($global:AppRootFolder + "\Documentation\CategoryId.json") -Encoding UTF8 | ConvertFrom-Json
    }
    
    Get-LanguageString "Category.$($script:CategoryIds."$($categoryId)")"
}

function Invoke-VerifyCondition
{
    param($obj, $prop, $objInfo)

    if(!$prop.Condition -or ($prop.Condition.Expressions | measure).Count -eq 0) { return $true}

    # Default condition type is or
    $type = ?: ($prop.Condition.type -eq "and") "and" "or"

    $defaultReturn = ?: ($type -eq "and") $true $false
    foreach($expression in $prop.Condition.Expressions)
    {
        if(!$expression.property) { continue }

        $tmpProp = $obj.PSObject.Properties | Where Name -eq $expression.property

        if(!$tmpProp)
        {
            if($expression.ignoreMissing -eq $true) { continue }
            return $false
        }

        if($expression.operator -eq "null")
        {            
            $tmpRet = $null -eq $tmpProp.Value
        }
        elseif($null -eq $expression.value)
        {
            # Value not specified. Check if the property is set
            $tmpRet = $null -ne $tmpProp.Value 
        }
        elseif($expression.operator -eq "ne")
        {
            $tmpRet = $obj."$($expression.property)" -ne $expression.value
        }
        elseif($expression.operator -eq "gt")
        {
            $tmpRet = $obj."$($expression.property)" -gt $expression.value
        }
        elseif($expression.operator -eq "ge")
        {
            $tmpRet = $obj."$($expression.property)" -ge $expression.value
        }
        elseif($expression.operator -eq "lt")
        {
            $tmpRet = $obj."$($expression.property)" -lt $expression.value
        }
        elseif($expression.operator -eq "le")
        {
            $tmpRet = $obj."$($expression.property)" -le $expression.value
        }
        elseif($expression.operator -eq "like")
        {
            $tmpRet = $obj."$($expression.property)" -like $expression.value
        }
        elseif($expression.operator -eq "notlike")
        {
            $tmpRet = $obj."$($expression.property)" -notlike $expression.value
        }
        else
        {
            # Default operator is eq
            $tmpRet = $obj."$($expression.property)" -eq $expression.value
        }
        
        if($tmpRet -eq $true -and $type -eq "or") { return $true }
        if($tmpRet -eq $false -and $type -eq "and") { return $false }
    }
    return $defaultReturn
}

function Invoke-TranslateSection
{
    param($obj, $sectionObject, $objInfo, $parent = $null)

    if($null -eq $parent -or $script:propLevel -lt 0)
    {
        $script:propLevel = 0
    }
    elseif($parent -ne $script:currentParent)# -and $parent.skipAddLevel -ne $true)
    {
        $script:propLevel++
    }

    foreach($prop in $sectionObject)
    {
        $valueData = $null
        $value = $null
        $valueSet = $false
        $useParentProp = $false
        $payloadFile = $false
        
        #if($prop.enabled -eq $false -and $objInfo.ShowDisabled -ne $true) { continue }

        if((Invoke-VerifyCondition $obj $prop $objInfo) -eq $false) 
        {
            Write-LogDebug "Condition returned false: $(($prop.Condition | ConvertTo-Json -Depth 50 -Compress))" 2
            continue
        }

        $obj = Get-CustomPropertyObject $obj $prop

        $rawValue = $obj."$($prop.entityKey)"

        if($prop.dataType -eq 8)
        {
            if($prop.nameResourceKey -eq "LearnMore") { continue }
            elseif($prop.nameResourceKey -eq "Empty") { $script:CurrentSubCategory = $null }
            elseif($prop.nameResourceKey -in $script:categoriesToIgnore) { continue }
            elseif($prop.nameResourceKey)
            {
                $tmpStr = (Get-LanguageString (?: $prop.nameResourceKey.Contains(".") $prop.nameResourceKey "SettingDetails.$($prop.nameResourceKey)"))
                if($tmpStr -and $tmpStr.Length -lt 75) # !!! categoriesToIgnore will take care of some
                {
                    $script:CurrentSubCategory = $tmpStr
                }
                elseif($tmpStr)
                {
                    Write-LogDebug "SubCategpry ignored based on length: $tmpStr" 2
                }
            }
            $script:propLevel = -1
            Invoke-ChildSections $obj $prop
            continue 
        }
        elseif($prop.dataType -eq 5) # complex Options
        {
            # Skip if disabled
            if($prop.enabled -eq $false -and $objInfo.ShowDisabled -ne $true) { continue }

            # Set sub-category to nameResourceKey value if EntityKey is empty?
            if(-not $prop.EntityKey -and $prop.nameResourceKey)
            {
                $script:propLevel = -1
                $script:CurrentSubCategory = (Get-LanguageString (?: $prop.nameResourceKey.Contains(".") $prop.nameResourceKey "SettingDetails.$($prop.nameResourceKey)"))
            }
            else
            {
                $script:propLevel--
            }

            foreach($tmpObj in $obj)
            {
                Invoke-TranslateSection $tmpObj $prop.complexOptions $objInfo -Parent $prop
            }
            continue                    
        }
        elseif($prop.dataType -eq 6) # Complex option based on sub property
        {
            # Skip if disabled
            if($prop.enabled -eq $false -and $objInfo.ShowDisabled -ne $true) { continue }

            # Use sub property if EntityKey is specified
            $script:propLevel--
            $propObj = $null
            if($prop.entityKey)
            {                
                $propObj = $obj.PSObject.Properties | Where Name -eq $prop.entityKey
            }

            foreach($tmpObj in (?: ($propObj -ne $null) $rawValue $obj))
            {
                Invoke-TranslateSection $tmpObj $prop.complexOptions $objInfo -Parent $prop
            }
            continue
        }
        elseif($prop.dataType -eq 9) # Label. Skip the label but add children
        {
            $script:propLevel--
            Invoke-ChildSections $obj $prop
            continue
        }
        elseif($prop.dataType -eq 10) # Information box
        {            
            continue
        }        
        elseif($prop.dataType -eq 101) # Static label. Language string id in value property
        {
            if($prop.value)
            {
                $value = Get-LanguageString $prop.value

                Add-PropertyInfo $prop $value $rawValue $rawValue
            }
        }
        elseif($prop.dataType -eq 107) # Static value. String in value property
        {
            if($prop.value)
            {
                Add-PropertyInfo $prop $prop.value $prop.value $prop.value
            }
        }
        elseif([String]::IsNullOrEmpty($prop.entityKey) -eq $false)
        {            
            $valueSet = ($rawValue -ne $null)
            $skipChildren = $false
            if($rawValue -eq $null -and ![String]::IsNullOrEmpty($prop.unconfiguredValue) -and $global:chkSetUnconfiguredValue.IsChecked)
            {
                $propValue = $prop.unconfiguredValue
                Add-NotConfiguredProperty $prop
            }
            elseif($rawValue -eq $null -and ![String]::IsNullOrEmpty($prop.defaultValue) -and $global:chkSetDefaultValue.IsChecked)
            {
                $propValue = $prop.defaultValue
            }
            elseif($rawValue -eq $null -and ![String]::IsNullOrEmpty($prop.emptyValueResourceKey) -and $global:chkSetDefaultValue.IsChecked)
            {
                $propValue = Get-LanguageString $prop.emptyValueResourceKey
            }
            else
            {
                $propValue = $rawValue
            }      

            $addPropertyInfo = $true            
            $customValue = Get-CustomProfileValue $obj $prop
            if($customValue -is [Boolean] -and $customValue -eq $false)
            {
                continue # Property added by Custom info. Skip processing
            }
            elseif(-not $customValue)
            {
                # Some properties are added but remarked to let it fall through to not supported handler                
                
                # Do linked property BEFORE property detection
                # Linked properties are based on navigationLinks so they are not an actual property
                if($prop.dataType -eq 4) # Linked certificate 
                {                    
                    $useParentProp = $true
                    # Use $script:currentObject since $obj could be a property on the original object
                    # Cert links are always specified on the main object
                    $cert = $null
                    if($script:offlineDocumentation -ne $true)
                    {
                        $cert = Invoke-GraphRequest -URL $script:currentObject."$($prop.entityKey)@odata.navigationLink" -ODataMetadata "minimal" -NoError
                    }
                    if($cert)
                    {
                        if($cert.value -is [Object[]])
                        {
                            $certs = @()
                            $cert.value | ForEach-Object { $certs += $_.displayName }
                            if($certs.Count -gt 0)
                            {
                                $value = ($certs -join $script:objectSeparator)
                            }
                        }
                        elseif($cert.displayName)
                        {
                            $value = $cert.displayName
                        }
                        $rawValue = $value
                    }
                    elseif($script:currentObject.'@ObjectFromFile' -eq $true)
                    {
                        if($script:currentObject."#CustomRef_$($prop.entityKey)")
                        {
                            $idx = $script:currentObject."#CustomRef_$($prop.entityKey)".IndexOf("|:|")
                            if($idx -gt -1)
                            {
                                $value = $script:currentObject."#CustomRef_$($prop.entityKey)".SubString(0,$idx)
                            }
                            else
                            {
                                $value = $script:currentObject."#CustomRef_$($prop.entityKey)"
                            }
                        }
                        else
                        {
                            $value = "##TBD - Linked Certificate"
                        }
                        $rawValue = $value
                    }
                }
                elseif($prop.dataType -eq 200) # Multi option based on boolean value
                {
                    $value = Get-LanguageString $prop.entityKey
                }
                elseif(($prop.allowMissing -ne $true) -and
                    ($prop.entityKey -ne ".") -and 
                    (-not ($obj.PSObject.Properties | Where Name -eq $prop.entityKey)) -and 
                    (-not ($obj.PSObject.Properties | Where Name -eq "$($prop.entityKey)@odata.navigationLink")))
                {
                    if($prop.enabled -ne $false)
                    {
                        Write-Log "Property with EntityKey $($prop.entityKey) is missing. Property will not be added!" 2
                    }
                    else
                    {
                        Write-LogDebug "Disabled property with EntityKey $($prop.entityKey) is missing. Property will not be added!" 2
                    }
                    continue
                }
                elseif($prop.dataType -eq 0) # Boolean
                {
                    $value = Invoke-TranslateBoolean $obj $prop
                }
                elseif($prop.dataType -eq 1) # Base64 e.g. certificate data
                {                    
                    if($prop.filenameEntityKey -and $obj."$($prop.filenameEntityKey)")
                    {
                        $value = $obj."$($prop.filenameEntityKey)"
                    }
                    else
                    {
                        $value = $obj."$($prop.EntityKey)"
                        if($value)
                        {
                            try
                            {
                                # Is this always Base64 string?
                                $value = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($value))
                            }
                            catch { }
                        }                        
                    }                    
                }
                elseif($prop.dataType -eq 2) # Multiline string e.g. XML file 
                {
                    if($prop.filenameEntityKey -and $obj."$($prop.filenameEntityKey)")
                    {
                        $value = $obj."$($prop.filenameEntityKey)"
                        $payloadFile = $true
                    }
                    else
                    {
                        $value = $obj."$($prop.EntityKey)"
                        if($value)
                        {
                            try
                            {
                                # Is this always Base64 string?
                                $value = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($value))
                            }
                            catch { }
                        }   
                    }
                }
                elseif($prop.dataType -eq 3) # Image
                {
                    # Better text?
                    $valueData = $propValue 
                    $value = ?: $propValue "Image file" $null
                }                                
                elseif($prop.dataType -eq 7) # omaSettingDateTime
                {
                    $value = $propValue # Might require formatting 
                }
                elseif($prop.dataType -eq 9) # DataType
                {
                    continue # Ignore @odata.type labels
                }
                elseif($prop.dataType -eq 10) # Label
                {
                    continue # Ignore labels
                }
                elseif($prop.dataType -eq 11) # App Picker
                {
                    #$value = $propValue
                }
                elseif($prop.dataType -eq 12) # Multiline String? Sometimes and array and sometimes a binary
                {
                    if(($propValue | measure).Count -gt 0)
                    {
                        $value = $propValue -join $script:objectSeparator
                    }
                }
                elseif($prop.dataType -eq 13) # Multi option
                {
                    $value = Invoke-TranslateMultiOption $obj $prop
                }
                elseif($prop.dataType -eq 14) # Int32
                {
                    $value = $propValue
                }
                elseif($prop.dataType -eq 15) # Int64
                {
                    $value = $propValue
                }
                elseif($prop.dataType -eq 16) # Option
                {
                    Invoke-TranslateOption $obj $prop | Out-Null
                    continue
                }
                elseif($prop.dataType -eq 19) # Option
                {
                    Invoke-TranslateOption $obj $prop | Out-Null
                    continue
                }
                elseif($prop.dataType -eq 20) # String
                {
                    $value = $propValue
                }
                elseif($prop.dataType -eq 21) # Table
                {
                    $value = Invoke-TranslateTable $obj $prop
                    continue
                }
                elseif($prop.dataType -eq 22) # Scale value e.g. 4 Years (Certificate validity period)
                {
                    $value = $propValue
                    $scaleEntityKey = ?? $obj."$($prop.scaleEntityKey)" $prop.defaultScale
                    if($scaleEntityKey)
                    {
                        $scaleOption = $prop.scaleOptions | Where value -eq $scaleEntityKey
                        if($scaleOption.nameResourceKey)
                        {
                            $value = "{0} {1}" -f $propValue, (Get-LanguageString "SettingDetails.$($scaleOption.nameResourceKey)")
                        }
                    }
                }
                #elseif($prop.dataType -eq 25) {}# Home screen
                # All data types above 100 are custom
                elseif($prop.dataType -eq 100) # Custom Edm.Duration
                {
                    $value = Invoke-TranslateDuration $obj $prop                    
                }
                elseif($prop.dataType -eq 102) # Culture name
                {
                    $value = Get-CutureLanguageString (?? $propValue $prop.unconfiguredValue)
                }
                elseif($prop.dataType -eq 103) # Boolean action but hide children on false
                {
                    $value = Invoke-TranslateBoolean $obj $prop

                    $skipChildren = ($propValue -eq $false)
                }                
                elseif($prop.dataType -eq 104) # Multi option based on boolean value
                {
                    $value = Invoke-TranslateMultiOptionBoolean $obj $prop

                    $skipChildren = ($propValue -eq $false)
                }
                elseif($prop.dataType -eq 105) # Multi option based on boolean value but $false if the value is selected
                {
                    $value = Invoke-TranslateMultiOptionBoolean $obj $prop $false

                    $skipChildren = ($propValue -eq $false)
                }
                elseif($prop.dataType -eq 106) # Array of languages (Culture)
                {
                    $arrTmp = @()
                    foreach($tmpLng in $propValue)
                    {
                        $arrTmp += Get-CutureLanguageString $tmpLng
                    }
                    $value = $arrTmp -join $script:objectSeparator
                }                   
                elseif($prop.dataType -eq 108) # String with format
                {
                    $value = $propValue
                    if($prop.formatStringKey) {
                        $str = Get-LanguageString $prop.formatStringKey
                        if($str)
                        {
                            $value = $str -f $propValue
                        }
                    }
                }
                else
                {
                    Write-Log "Unsupported property '$((Get-LanguageString "SettingDetails.$($prop.nameResourceKey)"))' ($($prop.nameResourceKey)) for object property $($prop.entityKey). Type: $($prop.dataType)" 2
                    $value = $propValue
                }
            }
            else
            {
                $value = $customValue.Value
                $rawValue = $customValue.RawValue
                $valueSet = ($rawValue -ne $null)
                $addPropertyInfo = $customValue.AddPropertyInfo
            }

            if($addPropertyInfo)
            {
                Add-PropertyInfo (?: ($useParentProp -and $parent) $parent $prop) $value $rawValue 

                if($payloadFile -eq $true -and $obj.payload)
                {
                    # Add payload file conetent as a property
                    $tmpProp = [PSCustomObject]@{
                        nameResourceKey = "uploadResult"
                        descriptionResourceKey = ""
                        entityKey = "payloadData"
                        dataType = 20
                        booleanActions = 0
                        category = $prop.Category
                    }
                    
                    $propValue = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($obj.payload))
        
                    Add-PropertyInfo $tmpProp $propValue -originalValue $obj.payload
                }
            }
        }
        else
        {
            Write-Log "No property entity key: $($prop.dataType) ($($prop.nameResourceKey))"
        }

        if($valueSet -and $skipChildren -ne $true)
        {
            Invoke-ChildSections $obj $prop
        }
    }

    if($null -ne $parent -and $parent -ne $script:currentParent -and $script:propLevel -gt 0)
    {
        $script:propLevel--
    }
}

function Get-CutureLanguageString
{
    param($culture)

    if(!$culture)
    {
        return
    }

    try
    {
        if($culture -eq "os-default")
        {
            return Get-LanguageString "Autopilot.OOBE.useOSDefaultLanguage"
        }
        elseif($culture -eq "user-select")
        {
            return Get-LanguageString "Autopilot.OOBE.userSelect"
        }
        else
        {
            if(!$script:languageStrings) { Get-LanguageString } # This will load the language strings

            if($script:languageStrings.Languages.$culture)
            {
                return $script:languageStrings.Languages.$culture
            }
            $lngArr = $culture.Split('-')
            if($lngArr.Length -eq 3)
            {
                if($script:languageStrings.Languages."$(($lngArr[0]-$lngArr[1]))") 
                {
                    return $script:languageStrings.Languages."$(($lngArr[0]-$lngArr[1]))"
                }
            }

            if($lngArr.Length -gt 1 -and $script:languageStrings.Languages."$(($lngArr[0]))") 
            {
                return $script:languageStrings.Languages."$(($lngArr[0]))"
            }

            Write-Log "Translated language for $culture not found" 2
            ([cultureinfo]$culture).EnglishName
        }
    }
    catch{}
}

function Add-PropertyInfo
{
    param($prop, $value, $originalValue, $jsonValue, $tableValue)

    if($prop.Category -eq "1000")
    {
        Add-BasicPropertyValue (Get-LanguageString $prop.nameResourceKey) $value
        return
    }

    $script:objectSettingsData += Get-PropertyInfo $prop $value $originalValue $jsonValue $tableValue

    Invoke-CustomPostAddValue $prop
}

function Add-PropertyInfoObject
{
    param($propInfo)

    if($propInfo -eq $null) { return }

    if($prop.dataType -eq 99)
    {

    }

    $script:objectSettingsData += $propInfo
}

function Get-PropertyInfo
{
    param($prop,$value,$originalValue, $jsonValue, $tableValue)

    if($prop.nameResource)
    {
        $name = $prop.nameResource
    }
    else
    {
        $name = Get-LanguageString (?: $prop.nameResourceKey.Contains(".") $prop.nameResourceKey "SettingDetails.$($prop.nameResourceKey)")
    }

    $description = ""
    if($prop.descriptionResource)
    {
        $description = $prop.descriptionResource
    }
    elseif($prop.descriptionResourceKey)
    {
        $description = Get-LanguageString (?: $prop.descriptionResourceKey.Contains(".") $prop.descriptionResourceKey "SettingDetails.$($prop.descriptionResourceKey)")
    }

    $categoryStr = $null

    if($prop.category)
    {
        $categoryStr = Get-Category $prop.category
    }

    if(!$jsonValue -and $null -ne $rawValue -and "$($rawValue)" -ne "")
    {
        $jsonValue = $rawValue | ConvertTo-Json -Depth 50 -Compress
    }

    if($prop.emptyValueResourceKey)
    {
        $defValue = Get-LanguageString $prop.emptyValueResourceKey
    }
    else
    {    
        $defValue = $prop.defaultValue
    }

    return New-Object PSObject -Property @{ 
        Name=$name
        Description=$description
        Value=$value
        Category=$categoryStr
        SubCategory=$script:CurrentSubCategory
        Property=$prop.entityKey
        ValueSet=$valueSet
        DataType=$prop.dataType
        RawValue=$originalValue
        RawJsonValue=$jsonValue
        DefaultValue=$defValue
        FullValueTable = $tableValue 
        UnconfiguredValue = $prop.unconfiguredValue
        AlwaysAddValue = $prop.alwaysAddValue -eq $true
        Enabled=$prop.Enabled 
        EntityKey=$prop.EntityKey
        Level=$script:propLevel
    }
}

function Invoke-ChildSections
{
    param($obj, $sectionObject)

    $objTmp = Get-CustomChildObject $obj $sectionObject

    $tmpLevel = $script:propLevel
    Invoke-TranslateSection $objTmp $sectionObject.Children $objInfo -Parent $sectionObject
    
    $script:propLevel = $tmpLevel
    Invoke-TranslateSection $objTmp $sectionObject.ChildSettings $objInfo -Parent $sectionObject    
}

function Get-CustomProfileValue
{
    param($obj, $prop)

    foreach($docProvider in ($global:documentationProviders | Sort -Property Priority))
    {
        if($docProvider.GetCustomProfileValue)
        {
            $retObj = & $docProvider.GetCustomProfileValue $obj $prop $script:currentObject $script:propertySeparator $script:objectSeparator
            if($retObj -ne $null)
            {
                return $retObj
            }
        }
    }
}

function Invoke-CustomPostAddValue
{
    param($prop)

    foreach($docProvider in ($global:documentationProviders | Sort -Property Priority))
    {
        if($docProvider.PostAddValue)
        {
            $retObj = & $docProvider.PostAddValue $script:currentObject $prop $script:propertySeparator $script:objectSeparator
            if($retObj -ne $null)
            {
                return $retObj
            }
        }
    }
}

function Get-CustomPropertyObject
{
    param($obj, $prop)

    foreach($docProvider in ($global:documentationProviders | Sort -Property Priority))
    {
        if($docProvider.GetCustomPropertyObject)
        {
            # $script:currentObject is used to always send in the main object
            $retObj = & $docProvider.GetCustomPropertyObject $script:currentObject $prop
            if($retObj)
            {
                return $retObj
            }
        }
    }

    return $obj
}

function Get-CustomChildObject
{
    param($obj, $prop)

    foreach($docProvider in ($global:documentationProviders | Sort -Property Priority))
    {
        if($docProvider.GetCustomChildObject)
        {
            # $script:currentObject is used to always send in the main object
            $retObj = & $docProvider.GetCustomChildObject $script:currentObject $prop
            if($retObj)
            {
                return $retObj
            }
        }
    }

    return $obj
}

function Invoke-InitDocumentation
{
    foreach($docProvider in ($global:documentationProviders | Sort -Property Priority))
    {
        if($docProvider.InitializeDocumentation)
        {
            & $docProvider.InitializeDocumentation
        }
    }
}

function Add-CustomProfileProperties
{
    param($obj)

    foreach($docProvider in ($global:documentationProviders | Sort -Property Priority))
    {
        if($docProvider.AddCustomProfileProperty)
        {
            if((& $docProvider.AddCustomProfileProperty $obj $script:propertySeparator $script:objectSeparator))
            {
                return
            }
        }
    }
}

function Get-CustomIgnoredCategories 
{
    param($obj)

    $script:categoriesToIgnore = @(
            #microsoft.graph.windows10EndpointProtectionConfiguration
            "defenderSecurityCenterContactOptionsText",
            "globalConfigurationsDescription","generalNetworkSettingsHeader",
            "firewallCreateRules","exploitGuardCFHeadingText","exploitGuardNFTitle",
            "exploitGuardEPExplainationPart1","exploitGuardEPExplainationPart2","exploitGuardEPExplainationPart3","exploitGuardEPExplainationPart4",
            "defenderSecurityCenterSubHeaderText","defenderSecurityCenterITContactInformationSubHeaderText",
            "windows10EndpointProtectionDeviceGuardLearnMore",
            #microsoft.graph.windows10GeneralConfiguration
            "win10DefaultPrivacyHeader",
            "dfciBuiltinHeaderDescName"
            
            )
}

function Add-DefenderFirewallSettings
{
    param($fwSettings, $fwType)

    foreach($fwProp in ($fwSettings.PSObject.Properties | Where { $_.Name -like "*Blocked" -or $_.Name -like '*NotMerged' }).Name)
    {            
        if($fwProp -like "*Blocked")
        {
            $blockedValue = $fwSettings.$fwProp
            $propPre = $fwProp.SubString(0, $fwProp.Length - 7)
            if(($fwSettings.PSObject.Properties | Where { $_.Name -eq "$($propPre)Required" }))
            {
                $nonBlockedValue = $fwSettings."$($propPre)Required"
            }
            elseif(($fwSettings.PSObject.Properties | Where { $_.Name -eq "$($propPre)Allowed" }))
            {
                $nonBlockedValue = $fwSettings."$($propPre)Allowed"
            }
            else { continue }

            $fwPropName = "firewallSyntheticProfile$($fwType)$($propPre)"                
        }
        else
        {
            $blockedValue = $fwSettings.$fwProp
            $propPre = $fwProp.SubString(0, $fwProp.Length - 9)
            if(($fwSettings.PSObject.Properties | Where { $_.Name -eq "$($propPre)Merged" }))
            {
                $nonBlockedValue = $fwSettings."$($propPre)Merged"
            }
            else { continue }

            $fwPropName = "firewallSyntheticProfile$($fwType)$($propPre)Merge"
        }

        $fwValue =  "notConfigured"
        if($blockedValue -eq $true -and $nonBlockedValue -eq $false)
        { 
            $fwValue = "blocked"
        }  
        elseif($blockedValue -eq $false -and $nonBlockedValue -eq $true)
        { 
            $fwValue = "allowed"
        }
        $obj | Add-Member Noteproperty -Name $fwPropName -Value $fwValue -Force            
    }
}


function Get-ConditionalLaunchSetting
{
    param($obj, $lngId, $prop, $actionLngId, [switch]$SkipValue)

    if($null -eq $obj."$($prop)" -or ($obj."$($prop)" -is [String] -and $obj."$($prop)" -eq "notConfigured")) { return }
    elseif($obj."$($prop)" -is [String] -and $obj."$($prop)".StartsWith("P"))
    {
        $ts = Get-DurationValue $obj."$($prop)" -ReturnTimeSpan
        if($prop -eq "periodOfflineBeforeAccessCheck")
        {
            $settingValue = $ts.TotalMinutes
        }
        else
        {
            $settingValue = $ts.TotalDays
        }
    }
    elseif($SkipValue -ne $true)
    {
        $settingValue = $obj."$($prop)"
    }
    else
    {
        $settingValue = $null
    }

    [PSCustomObject]@{
        Setting=(Get-LanguageString (?: $lngId.Contains(".") $lngId "SettingDetails.$lngId"))
        Value=$settingValue
        Action=(Get-LanguageString (?: $actionLngId.Contains(".") $actionLngId "SettingDetails.$actionLngId"))
    }
}

function Invoke-TranslateBoolean
{
    param($obj, $prop)

    $propValue = (?? ($obj."$($prop.entityKey)") $prop.unconfiguredValue)

    if($propValue -is [String] -and ($propValue -eq "true" -or $propValue -eq "false"))
    {
        $propValue = [Boolean]::Parse($propValue)
    }

    if("$propValue" -eq "notConfigured")
    {
        Get-LanguageString "BooleanActions.notConfigured"
    }
    elseif($prop.booleanActions -eq 0 -and $propValue -eq $true)
    {
        Get-LanguageString "BooleanActions.allow"        
    }
    elseif($prop.booleanActions -eq 1 -and $propValue -eq $true)
    {
        Get-LanguageString "BooleanActions.require"        
    }
    elseif($prop.booleanActions -eq 2 -and $propValue -eq $true)
    {
        Get-LanguageString "BooleanActions.enable"        
    }
    elseif($prop.booleanActions -eq 3 -and $propValue -eq $true)
    {
        Get-LanguageString "BooleanActions.block"        
    }
    elseif($prop.booleanActions -eq 4 -and $propValue -eq $true)
    {
        Get-LanguageString "BooleanActions.configured"        
    }
    elseif($prop.booleanActions -eq 5 -and $propValue -eq $true)
    {
        Get-LanguageString "BooleanActions.disable"        
    }
    elseif($prop.booleanActions -eq 6 -and $propValue -eq $true)
    {
        Get-LanguageString "BooleanActions.limit"        
    }
    elseif($prop.booleanActions -eq 7 -and $propValue -eq $true)
    {
        Get-LanguageString "BooleanActions.show"        
    }
    elseif($prop.booleanActions -eq 8 -and $propValue -eq $true)
    {
        Get-LanguageString "BooleanActions.hide"        
    }
    elseif($prop.booleanActions -eq 9 -and $propValue -eq $true)
    {
        Get-LanguageString "BooleanActions.yes"        
    }
    # Custom 
    elseif($prop.booleanActions -eq 100)
    {
        Get-LanguageString (?: $propValue "BooleanActions.block" "BooleanActions.allow")
    }    
    elseif($prop.booleanActions -eq 101)
    {
        Get-LanguageString (?: $propValue "BooleanActions.require" "SettingDetails.notRequired")
    }
    elseif($prop.booleanActions -eq 102)
    {
        Get-LanguageString (?: $propValue "BooleanActions.enable" "BooleanActions.disable")
    }
    elseif($prop.booleanActions -eq 107 -and $propValue -eq $true)
    {
        Get-LanguageString (?: $propValue "BooleanActions.show" "BooleanActions.hide")
    }
    elseif($prop.booleanActions -eq 108 -and $propValue -eq $true)
    {
        Get-LanguageString (?: $propValue "BooleanActions.hide" "BooleanActions.show")
    }
    elseif($prop.booleanActions -eq 109)
    {
        Get-LanguageString (?: $propValue "BooleanActions.yes" "SettingDetails.no")
    }
    elseif($prop.booleanActions -eq 110)
    {
        Get-LanguageString (?: $propValue "SettingDetails.no" "BooleanActions.yes")
    }
    elseif($prop.booleanActions -eq 120)
    {
        Get-LanguageString (?: $propValue "SettingDetails.onOption" "SettingDetails.offOption")
    }
    elseif($prop.booleanActions -eq 200)
    {
        Get-LanguageString (?: $propValue "BooleanActions.allow" "BooleanActions.block")
    }    
    elseif($prop.booleanActions -eq 201)
    {
        Get-LanguageString (?: $propValue "SettingDetails.notRequired" "BooleanActions.require")
    }
    elseif($prop.booleanActions -eq 220)
    {
        Get-LanguageString (?: $propValue "SettingDetails.offOption" "SettingDetails.onOption")
    }
    else
    {
        Add-NotConfiguredProperty $prop
        Get-LanguageString "BooleanActions.notConfigured"
    }
}

function Add-NotConfiguredProperty
{
    param($prop)

    # Add not configured prop to the base object
    if(-not ($script:currentObject.PSObject.Properties | Where Name -eq "@UnconfiguredProperties"))
    {
        $script:currentObject | Add-Member Noteproperty -Name "@UnconfiguredProperties" -Value @() -Force 
    }

    $script:currentObject.'@UnconfiguredProperties' += $prop
}

function Invoke-TranslateOption
{
    param($obj, $prop, [Switch]$SkipOptionChildren, $propValue = $null)

    if(-not $propValue)
    {
        $propValue = $obj."$($prop.entityKey)"
    }

    if($obj.defenderSecurityCenterDisableRansomwareUI -eq $true)
    {
        $obj.defenderSecurityCenterDisableRansomwareUI = "blockOption"
    }    

    foreach($option in $prop.options)
    {
        if("$propValue" -eq "$($option.Value)") # Compare strings since some properties is boolean but option value is string
        {
            if($option.nameResource)
            {
                $optionValue = $prop.nameResource
            }
            elseif($option.displayText)
            {
                $optionValue = $option.displayText
            }
            elseif($option.nameResourceKey)
            {
                if($option.nameResourceKey -eq "notConfigured")
                {
                    Add-NotConfiguredProperty $prop
                }

                $optionValue = (Get-LanguageString (?: $option.nameResourceKey.Contains(".") $option.nameResourceKey "SettingDetails.$($option.nameResourceKey)"))
            }
            else
            {
                $optionValue = $option.Value
            }
            
            @{
                Option=$option
                Value=$optionValue
            }

            Add-PropertyInfo $prop $optionValue $propValue
            if($SkipOptionChildren -ne $true)
            {
                Invoke-ChildSections (Get-CustomChildObject $obj $prop) $option
            }
            break
        }
    }

    # This does not work. Sometime's boolean children are added on $true and sometimes on $false. Depends on the property type
    # This WILL cause "disabled properties" in the documentation
    # Log for now to identify how many properties it is 

    if($propValue -is [Boolean] -and $prop.ChildSettings.Count -gt 0)
    {
        Write-Log "Child properties for boolean $($prop.EntityKey) value=$($propValue) added. Disabled items might be included." 2
    }

    #if($propValue -isnot [Boolean] -or $propValue -eq $true)
    #{
    #    # Skip Child Settings if the option value is not configured
        Invoke-ChildSections $obj $prop
    #}
}

function Invoke-TranslateMultiOption
{
    param($obj, $prop)

    if(($obj.PSObject.Properties | Where Name -eq $prop.entityKey))
    {
        $propValues = $obj."$($prop.entityKey)"
        if($propValues -is [String])
        {
            $propValues = $propValues.Split(',')
        }
    }
    elseif( $prop.entityKey -like "*List")
    {   
        # Should NOT be used!     
        $tmpProp = $prop.entityKey.SubString(0, $prop.entityKey.Length - 4)
        $propValues = $obj."$($tmpProp)"
    }

    $selectedValues = @()
    foreach($propValue in $propValues)
    {
        $option = $prop.Options | Where Value -eq $propValue
        if($option)
        {
            if($option.nameResource)
            {
                $selectedValues += $option.nameResource
            }
            else
            {
                $selectedValues += (Get-LanguageString (?: $option.nameResourceKey.Contains(".") $option.nameResourceKey "SettingDetails.$($option.nameResourceKey)"))
            }
        }
    }

    if($selectedValues.Count -gt 0)
    {
        ($selectedValues -join $script:propertySeparator)
    }
    else
    {
        Add-NotConfiguredProperty $prop
        Get-LanguageString "BooleanActions.notConfigured"
    }
}

function Invoke-TranslateMultiOptionBoolean
{
    param($obj, $prop, $selectedValue = $true)

    $propObj = $obj."$($prop.entityKey)"

    $selectedValues = @()
    foreach($propValue in ($propObj.PSObject.Properties).Name)
    {
        if($propObj.$propValue -isnot [Boolean]) { continue }
        $option = $prop.options | Where value -eq $propValue
        if(-not $option) { continue }

        if($propObj.$propValue -eq $selectedValue)
        {
            if($option.nameResource)
            {
                $selectedValues += $option.nameResource
            }
            else
            {
                $selectedValues += (Get-LanguageString (?: $option.nameResourceKey.Contains(".") $option.nameResourceKey "SettingDetails.$($option.nameResourceKey)"))
            }
        }
    }
    
    if($selectedValues.Count -gt 0)
    {
        ($selectedValues -join $script:propertySeparator)
    }
    else
    {
        Get-LanguageString "BooleanActions.notConfigured"
    }
}

function Invoke-TranslateTable
{
    param($obj, $prop)

    if($prop.entityKey -eq ".")
    {
        $propValue = $obj
    }
    else
    {
        $propValue = $obj."$($prop.entityKey)"
    }

    $items = @()
    
    $itemFullValue = @()
    foreach($item in $propValue)
    {
        $itemValues = @()
        $htFullPropInfo = [ordered]@{}
        foreach($column in $prop.Columns)
        {
            if($column.metadata.entityKey -eq "unusedForSingleItems")
            {
                $itemValues += $item
            }
            elseif($column.metadata.entityKey -eq $prop.entityKey -and ($prop.Columns | measure).Count -eq 1)
            {
                # Some tables has the same EntityKey for the table and the column. That will generate the wrong value
                $itemValues += $item 
            }
            elseif(($prop.Columns | measure).Count -eq 1 -and $item."$($column.metadata.entityKey)" -eq $null -and $obj."$($column.metadata.entityKey)" -eq $null -and $item -is [String])
            {
                # Not sure how correct this is but some tables has one column with and EntityKey but the objects is a string list
                $itemValues += $item
            }
            else
            {
                if(($item.PSObject.Properties | Where Name -like $column.metadata.entityKey))
                {
                    $itemTmpVal = $item."$($column.metadata.entityKey)"
                }
                else
                {
                    $itemTmpVal = $obj."$($column.metadata.entityKey)"
                }
                $itemValues += $itemTmpVal
                if($prop.Columns.Count -gt 1)
                {
                    if($column.metadata.nameResourceKey)
                    {
                        $htFullPropInfo.Add((?? (Get-LanguageString (?: $column.metadata.nameResourceKey.Contains(".") $column.metadata.nameResourceKey "SettingDetails.$($column.metadata.nameResourceKey)")) $column.metadata.entityKey), ($itemTmpVal -join $script:propertySeparator))
                    }
                    else
                    {
                        Write-Log "Property $(?? $prop.nameResourceKey $prop.entityKey) does not have nameResourceKey on one of the columns" 2
                    }
                }
            }
        }
        if($htFullPropInfo.Count -gt 0)
        {
            $itemFullValue += [PSCustomObject]$htFullPropInfo
        }

        if($prop.separator)
        {
            $items += $itemValues -join $prop.separator
        }
        else
        {
            $items += $itemValues -join $script:propertySeparator
        }
    }

    if($items.Count -gt 0)
    {
        $params = @{}
        if($itemFullValue.Count -gt 0)
        {
            $params.Add("tableValue", $itemFullValue)
        }
        
        if((-not $prop.nameResourceKey -or $prop.nameResourceKey -eq "Empty") -and $prop.columns[0].metadata.nameResourceKey)
        {
            Add-PropertyInfo $prop.columns[0].metadata ($items -join $script:objectSeparator) $propValue @params
        }
        else
        {
            Add-PropertyInfo $prop ($items -join $script:objectSeparator) $propValue @params
        }        
    }
    else
    {
        if((-not $prop.nameResourceKey -or $prop.nameResourceKey -eq "Empty") -and $prop.Columns[0].metadata.nameResourceKey)
        {
            Add-PropertyInfo $prop.Columns[0].metadata $null
        }
        else
        {
            Add-PropertyInfo $prop $null
        }
    }

    Invoke-ChildSections $obj $prop
}

function Invoke-TranslateDuration
{
    param($obj, $prop)

    (Get-DurationValue  $obj."$($prop.entityKey)")
}

function Get-DurationValue
{   param($durationValue, [Switch]$ReturnTimeSpan)

    if(-not $durationValue -or !$durationValue.StartsWith("P")) { return "0" }


    $arr = @('P','T','Y','D','H','M','S')
    $values = $durationValue.Split($arr)

    $years = 0
    $days = 0
    $hours = 0
    $minutes = 0
    $seconds = 0

    $i = 0
    foreach($tmp in $arr)
    {
        if($durationValue.Contains($tmp))
        {
            if($ReturnTimeSpan -eq $true)
            {
                if($tmp -eq "Y") { $years = [int]$values[$i] }
                elseif($tmp -eq "D") { $days = [int]$values[$i] }
                elseif($tmp -eq "H") { $hours = [int]$values[$i] }
                elseif($tmp -eq "M") { $minutes = [int]$values[$i] }
                elseif($tmp -eq "S") { $seconds = [int]$values[$i] }
            }
            elseif(![String]::IsNullOrEmpty($values[$i])) { return $values[$i] }
            $i++
        }
    }

    if($ReturnTimeSpan -eq $true)
    {
        $days += ($years * 365) # Not really 100% true
        [timespan]::new($days, $hours, $minutes, $seconds)
    }
    else
    {
        return "0"
    }
}

function Add-Header
{
    param($sectionObj, [switch]$Category)

    if($sectionObj.nameResourceKey)
    {
        $script:CurrentSubCategory = (Get-LanguageString "SettingDetails.$($sectionObj.nameResourceKey)")
    }
}

#endregion

#region 
function Invoke-TranslateScheduledActionType
{
    param($scheduleActionObjects)

    $category = Get-LanguageString "Category.complianceActionsLabel"

    foreach($actionRule in $scheduleActionObjects)
    {
        foreach($actionConfig in $actionRule.scheduledActionConfigurations)
        {
            $notificationTemplate = $null
            $notificationTemplateId = $null
            $additionalNotifications = $null
            $additionalNotificationsList = $null

            if($actionConfig.actionType -eq "notification")
            {
                # Had to change the key of the language string. 
                # Json in 5.x does not support importing json data with two keys with the same name e.g.
                # notification and Notification 
                # Not a good workaround but -AsHashtable is only available in PowerShell 6.x + and JavaScriptSerializer is not good at encoding
                $notificationEnumString = "emailNotification"
            }
            else
            {
                $notificationEnumString = $actionConfig.actionType
            }

            $actionType = Get-LanguageString "ScheduledAction.$($notificationEnumString)"
            
            if($actionConfig.gracePeriodHours -eq 0)
            {
                $schedule = Get-LanguageString "ScheduledAction.List.immediately"
            }
            else
            {
                # Always sets the gracePeriodHours property but always uses days
                $schedule = ((Get-LanguageString "ScheduledAction.List.gracePeriodDays") -f ($actionConfig.gracePeriodHours/24))
            }

            if($actionConfig.actionType -eq "notification")
            {
                if($actionConfig.notificationTemplateId -ne [Guid]::Empty)
                {
                    $notificationTemplate = Get-LanguageString "ScheduledAction.Notification.selected"
                    $notificationTemplateId = $actionConfig.notificationTemplateId
                }
                else
                {
                    $notificationTemplate = Get-LanguageString "ScheduledAction.Notification.noneSelected"
                }

                if($actionConfig.notificationMessageCCList.Count -gt 0)
                {
                    $additionalNotifications = ((Get-LanguageString "ScheduledAction.Notification.numSelected") -f $actionConfig.notificationMessageCCList.Count)
                    $additionalNotificationsList = $actionConfig.notificationMessageCCList -join ","
                }
                else
                {
                    $additionalNotifications = Get-LanguageString "ScheduledAction.Notification.noneSelected"
                }
            }

            $objClone = $actionConfig | ConvertTo-Json -Depth 50 | ConvertFrom-Json

            Remove-Property $objClone "Id"
            foreach($prop in $objClone.PSObject.Properties)
            {
                if($prop.Name -like "*@odata*") 
                { 
                    Remove-Property $objClone $prop.Name    
                }
            }            

            # ToDo: Resolve MessageTemplateId and EmailCCIds to actual object names
            $script:objectComplianceActionData += New-Object PSObject -Property @{
                IdStr = ($objClone | ConvertTo-Json -Depth 50 -Compress)
                Action = $actionType
                Schedule = $schedule
                MessageTemplate = $notificationTemplate
                MessageTemplateId = $notificationTemplateId
                EmailCC = $additionalNotifications
                EmailCCIds = $additionalNotificationsList
                Category=$category
                RawJsonValue=($actionConfig | ConvertTo-Json -Depth 50 -Compress)
            }
        }
    }
}
#endregion

#region Assignments
function Invoke-TranslateAssignments
{
    param($obj)

    if($global:chkExcludeAssignments.IsChecked -eq $true) { return }

    $filtersInfo = $null

    $included = @()
    $excluded = @()
    $tmpAssignments = @()

    $groupIds = @()
    $filterIds = @()

    $category = Get-LanguageString "TableHeaders.assignments"

    $groupModeInclude = Get-LanguageString "TableHeaders.includedGroups"
    $groupModeExnclude = Get-LanguageString "TableHeaders.excludedGroups"

    $noFilter = Get-LanguageString "AssignmentFilters.noFilters"
    $filterInclude = Get-LanguageString "SettingDetails.include"
    $filterExclude = Get-LanguageString "SettingDetails.exclude"

    foreach($assignment in $obj.assignments)
    {
        if($assignment.target.groupId -and $assignment.target.groupId -notin $groupIds)
        {
            $groupIds += $assignment.target.groupId
        }

        if($assignment.target.deviceAndAppManagementAssignmentFilterId -and ($assignment.target.deviceAndAppManagementAssignmentFilterId -notin $filterIds))
        {
            $filterIds += $assignment.target.deviceAndAppManagementAssignmentFilterId
        }
    }

    $groupInfo = $null

    if($groupIds.Count -gt 0 -and $script:offlineDocumentation -ne $true)
    {
        $ht = @{}
        $ht.Add("ids", @($groupIds | Unique))

        $body = $ht | ConvertTo-Json

        $groupInfo = (Invoke-GraphRequest -Url "/directoryObjects/getByIds?`$select=displayName,id" -Content $body -Method "Post").Value
    }
    
    if(($null -eq $groupInfo -or ($groupInfo | measure).Count -eq 0) -and $obj."@ObjectFromFile" -eq $true -and $script:migTable)
    {
        ### Get group info from mig table when documenting from file if there's no access to the environment
        $groupInfo = $script:migTable.Objects | Where Type -eq "Group" 
    }    

    if($filterIds.Count -gt 0)
    {        
        if($script:offlineDocumentation -eq $true)
        {
            if($script:offlineObjects["AssignmentFilters"])
            {
                $filtersInfo = $script:offlineObjects["AssignmentFilters"] | Where { $_.Id -in $filterIds }
            }
            else
            {
                Write-Log "No assignment filters loaded for Offline documentation. Check export folder" 2
            }
        }
        else
        {
            $batchInfo = @{}
            $requests = @()
            #{"requests":[{"id":"<FilterID>","method":"GET","url":"deviceManagement/assignmentFilters/<FilterID>?$select=displayName"}]}
            foreach($filterId in $filterIds)
            {
                $requests += [PSCustomObject]@{
                    id = $filterId
                    method = "GET"
                    "url" = "deviceManagement/assignmentFilters/$($filterId)?`$select=displayName"
                }
            }
            $batchInfo = @{"requests"=$requests}
            $jsonBody = $batchInfo | ConvertTo-Json

            $filtersInfo = (Invoke-GraphRequest -Url "/`$batch" -Content $jsonBody -Method "Post").responses.body
        }
    }
    
    foreach($assignment in $obj.assignments)
    {
        $groupMode = $null
        $groupName = $null

        $filterName = $null
        $filterMode = $null
        $fullValueTable = $null

        # ToDo: Not an OK way of specifying this!
        if(($assignment.PSObject.Properties | Where Name -eq "intent") -and
            ($assignment.PSObject.Properties | Where Name -eq "settings"))
        {
            $assignmentType = "mobileAppAssignmentSettings"            
        }
        else
        {
            $assignmentType = "genericAssignment"    
        }

        if($assignment.target.'@odata.type' -eq "#microsoft.graph.exclusionGroupAssignmentTarget")
        {
            $groupMode = "exclude"
        }
        else
        {
            $groupMode = "include"
        }
    
        if($assignment.target.GroupId)
        {
            $groupName = ($groupInfo | Where id -eq $assignment.target.GroupId).displayName
            if(-not $groupName)
            {
                $groupName = $assignment.target.GroupId
            }
        }
        elseif($assignment.target.'@odata.type' -eq "#microsoft.graph.allDevicesAssignmentTarget")
        {
            $groupName = Get-LanguageString "SettingDetails.allDevices"
        }
        elseif($assignment.target.'@odata.type' -eq "#microsoft.graph.allLicensedUsersAssignmentTarget")
        {
            $groupName = Get-LanguageString "SettingDetails.allUsers"
        }
        else
        {
            $groupName = "unknown" # Should not get here!    
        }

        if(($assignment.target.PSObject.Properties | Where Name -eq "deviceAndAppManagementAssignmentFilterId"))
        {        
            $filterName = $noFilter
            $filterMode = $noFilter
    
            if($assignment.target.deviceAndAppManagementAssignmentFilterId -and $filtersInfo)
            {
                $filtersObj = $filtersInfo | Where Id -eq $assignment.target.deviceAndAppManagementAssignmentFilterId
                if($filtersObj.displayName)
                {
                    $filterName = $filtersObj.displayName
                }

                if($assignment.target.deviceAndAppManagementAssignmentFilterType -eq "include")
                {
                    $filterMode = $filterInclude
                }
                else
                {
                    $filterMode = $filterExclude
                }
            }
        }

        # ToDo: Add support if Direct or from Policy Set

        # Add additional app assignment settings
        # Only add settings for Included assignments
        $assignmentSettingProps = [ordered]@{}
        if($assignmentType -eq "mobileAppAssignmentSettings" -and $groupMode -eq "include" -and $assignment.settings -ne $null)
        {            
            foreach($settingProp in @("useDeviceContext",
                                        "vpnConfigurationId",
                                        "uninstallOnDeviceRemoval",
                                        "isRemovable",
                                        "useDeviceLicensing",
                                        "androidManagedStoreAppTrackIds",
                                        "deliveryOptimizationPriority",
                                        "installTimeSettings",
                                        "notifications",
                                        "restartSettings"))
            {
                if(($assignment.settings.PSObject.Properties | Where Name -eq $settingProp))
                {
                    if($settingProp -eq "useDeviceLicensing")
                    {
                        if($assignment.settings.$settingProp -eq $true)
                        {
                            $value = Get-LanguageString "SettingDetails.licenseTypeDevice"
                        }
                        else
                        {
                            $value = Get-LanguageString "SettingDetails.licenseTypeUser"
                        }
                    }
                    elseif($settingProp -eq "restartSettings")
                    {
                        if($null -eq $assignment.settings.$settingProp)
                        {
                            $value = Get-LanguageString "SettingDetails.disabledOption"
                        }
                        else
                        { 
                            $valueArr = @()    
                            #$valueArr += Get-LanguageString "SettingDetails.enabledOption"
                            $valueArr += "$((Get-LanguageString "Assignment.RestartGracePeriod.durationInMinutes"))=$($assignment.settings.restartSettings.gracePeriodInMinutes)"
                            $valueArr += "$((Get-LanguageString "Assignment.RestartGracePeriod.countdownDialog"))=$($assignment.settings.restartSettings.countdownDisplayBeforeRestartInMinutes)"
                            
                            if($null -eq $assignment.settings.restartSettings.restartNotificationSnoozeDurationInMinutes)
                            {
                                $valueArr += "$((Get-LanguageString "Assignment.RestartGracePeriod.allowSnooze"))=$((Get-LanguageString "SettingDetails.no"))"
                            }
                            else
                            {
                                $valueArr += "$((Get-LanguageString "Assignment.RestartGracePeriod.allowSnooze"))=$((Get-LanguageString "SettingDetails.yes"))"
                                $valueArr += "$((Get-LanguageString "Assignment.RestartGracePeriod.snoozeDurationInMinutes"))=$($assignment.settings.restartSettings.restartNotificationSnoozeDurationInMinutes)"
                            }
                            $value = $valueArr -join $script:objectSeparator
                        }
                    }
                    elseif($settingProp -eq "notifications")
                    {
                        $value = ?? (Get-LanguageString "AppResources.AssignmentToast.$($assignment.settings.$settingProp)") $assignment.settings.$settingProp
                    }
                    elseif($settingProp -eq "installTimeSettings")
                    {                        
                        $asap = Get-LanguageString "Assignment.SoftwareInstallationTime.defaultTime"
                        $startValue = $asap
                        $value = $asap

                        if($assignment.settings.installTimeSettings)
                        {
                            if($assignment.settings.installTimeSettings.startDateTime)
                            {
                                $instTime = Get-Date $assignment.settings.installTimeSettings.startDateTime
                                
                                if($assignment.settings.installTimeSettings.useLocalTime -eq $false)
                                {
                                    $hours = ($instTime.ToUniversalTime() - $instTime).Hours
                                    $instTime = $instTime.AddHours($hours)
                                }
                                $startValue = "$($instTime.ToShortDateString()) $($instTime.ToShortTimeString())" 
                            }

                            if($assignment.settings.installTimeSettings.deadlineDateTime)
                            {
                                $endTime = Get-Date $assignment.settings.installTimeSettings.deadlineDateTime
                                
                                if($assignment.settings.installTimeSettings.useLocalTime -eq $false)
                                {
                                    $hours = ($endTime.ToUniversalTime() - $endTime).Hours
                                    $endTime = $endTime.AddHours($hours)
                                }
                                $value = "$($endTime.ToShortDateString()) $($endTime.ToShortTimeString())" 
                            }
                        }

                        $assignmentSettingProps.Add("startTimeColumnLabel", $startValue)
                        if($assignment.Intent -eq "available")
                        {
                            continue # No install deadline on available assignments
                        }
                    }                    
                    elseif($settingProp -eq "deliveryOptimizationPriority")
                    {
                        $tmpStr = Get-LanguageString "AppResources.DeliveryOptimizationPriority.displayText"
                        if($assignment.settings.$settingProp -ne "foreground")
                        {
                            $tmpType = Get-LanguageString "AppResources.DeliveryOptimizationPriority.backgroundNormal"
                        }
                        else
                        {
                            $tmpType = Get-LanguageString "AppResources.DeliveryOptimizationPriority.foreground"
                        }
                        $value = $tmpStr -f $tmpType
                    }
                    elseif($assignment.settings.$settingProp -eq "notConfigured")
                    {
                        $value = Get-LanguageString "BooleanActions.notConfigured"
                    }                    
                    else
                    {
                        $value = $assignment.settings.$settingProp
                    }
                    $assignmentSettingProps.Add($settingProp, $value)
                }
            }
        }
        
        if($assignmentType -eq "genericAssignment")
        {            
            $assignObj = [PSCustomObject]@{
                GroupMode = (?: ($groupMode -eq "include") $groupModeInclude $groupModeExnclude)
                Group = $groupName
                Type = "GenericAssignment"
                Category = (?: ($groupMode -eq "include") $groupModeInclude $groupModeExnclude)
            }

            if($groupMode -eq "include")
            {
                $included += $assignObj
                if($filterMode -ne $null)
                {
                    $assignObj | Add-Member Noteproperty -Name "Filter" -Value $filterName -Force 
                    $assignObj | Add-Member Noteproperty -Name "FilterMode" -Value $filterMode -Force 
                }
            }
            else
            {
                $excluded += $assignObj
            }
        }
        else
        {
            $appAssignment = @{        
                Group =  $groupName
                GroupMode = Get-LanguageString "AssignmentAction.$($groupMode)"
                Category = Get-LanguageString "InstallIntent.$($assignment.Intent)"
                RawIntent = $assignment.Intent
                RawJsonValue = ($assignment | ConvertTo-Json -Depth 50 -Compress)
            }

            if($groupMode -eq "include")
            {
                $appAssignment.Add("Filter", $filterName)
                $appAssignment.Add("FilterMode", $filterMode)
            }            

            if($assignmentSettingProps.Count -gt 0)
            {
                $appAssignment.Add("Settings", $assignmentSettingProps)
            }
            
            $tmpAssignments += [PSCustomObject]$appAssignment
        }        
    }
    
    if($included.Count -gt 0)
    {
        $script:objectAssignments += $included
        <#
        $script:objectAssignments += [PSCustomObject]@{                
            GroupMode = Get-LanguageString "TableHeaders.includedGroups"
            Groups = ($included -join $script:objectSeparator)            
            Type = "GenericAssignment"
        }
        #>
    }

    if($excluded.Count -gt 0)
    {
        $script:objectAssignments += $excluded
        <#
        $script:objectAssignments += [PSCustomObject]@{
                
            GroupMode = Get-LanguageString "TableHeaders.excludedGroups"
            Groups = ($excluded -join $script:objectSeparator)
            Type = "GenericAssignment"
        }
        #>
    }

    if($tmpAssignments.Count -gt 0)
    {
        # Sort the items in the correct order
        foreach($intent in @("required","available","availableWithoutEnrollment","uninstall"))
        {
            $script:objectAssignments += $tmpAssignments | Where RawIntent -eq $intent
        }
    }
}
#endregion

#region Applications

function Get-GraphAppType
{
    param($obj)

    if(-not $script:allAppTypes)
    {
        $fi = [IO.FileInfo]($global:AppRootFolder + "\Documentation\AppTypes.json")
        if(!$fi.Exists)
        {        
            return $false
        }
        $script:allAppTypes = Get-Content ($global:AppRootFolder + "\Documentation\AppTypes.json") | ConvertFrom-Json        
    }

    foreach($appType in ($script:allAppTypes | Where ODataType -eq $obj.'@OData.Type'))
    {
        if($appType.Condition)
        {
            if($obj."$($appType.Condition.Property)" -eq $appType.Condition.Value)
            {
                return $appType
            }
        }
        else
        {
            return $appType
        }
    }
}
#endregion

#region Custom Policy objects - base on json file named after OData.Type
function Invoke-TranslateCustomProfileObject
{
    param($obj, $fileName)

    $fi = [IO.FileInfo]($global:AppRootFolder + "\Documentation\ObjectInfo\$($fileName).json")
    if(!$fi.Exists)
    {        
        return $false
    }
    $jsonObj = Get-Content ($global:AppRootFolder + "\Documentation\ObjectInfo\$($fileName).json") | ConvertFrom-Json

    $platformType = Get-ObjectPlatformFromType $obj 

    Add-BasicDefaultValues $obj $objectType 

    Add-CustomProfileProperties $obj

    Invoke-TranslateSection $obj $jsonObj

    Add-BasicAdditionalValues $obj $objectType

    return $true
}
#endregion

function Add-CustomSettingObject
{
    param($settingsObj)

    $script:objectSettingsData += $settingsObj
}

function Save-DocumentationFile
{
    param($content, $fileName, [switch]$OpenFile)

    try
    {
        $content | Out-File -LiteralPath $fileName -Force -Encoding utf8 -ErrorAction Stop
        Write-Log "$fileName saved successfully"
        if($OpenFile -eq $true)
        {
            Invoke-Item $fileName
        }        
    }
    catch
    {
        Write-LogError "Failed to save file $fileName." $_.Exception
    }
}

#region Initiate documentation
function Show-DocumentationForm
{
    param(
        [Parameter(Mandatory=$true,ParameterSetName = "Objects")]
        $objects, 
        [Parameter(Mandatory=$true,ParameterSetName = "ObjectTypes")]
        $objectTypes,
        [Switch]
        [Parameter(Mandatory=$false,ParameterSetName = "Objects")]
        $SelectedDocuments,    
        [Switch]
        $ShowFolderSource 
        )

    $objectList = @()
    
    if($objects)
    {
        if($SelectedDocuments -eq $true)
        {
            $objectList += $objects
            $objects | ForEach-Object { $_.IsSelected = $true }
        }
        else
        {
            $objects | ForEach-Object {                
                # This will create a new "root" object so it doesn't update properties like IsSelected ect on the original object
                $item = [PSCustomObject]@{
                    Title =  $null
                }

                foreach($prop in $_.PSObject.Properties)
                {
                    $item | Add-Member Noteproperty -Name $prop.Name -Value $_."$($prop.Name)" -Force
                }

                $item.Title = (Get-GraphObjectName $_.Object $_.ObjectType) 
                $item.IsSelected = $true 
                $objectList += $item                
            }
        }
        $sourceType = "Objects"
    }
    elseif($objectTypes)
    {
        foreach($groupId in ($objectTypes | Select GroupId -Unique).GroupId)
        {
            if(-not $groupId) { continue }

            #$script:DocumentationLanguage = ?? $global:cbDocumentationLanguage.SelectedValue "en"
            $script:DocumentationLanguage = "en"
            $groupName = Get-ObjectTypeString -ObjectType $groupId
            if(-not $groupName)
            {
                Write-Log "Group id $groupId could not be translated" 2
                $groupName = $groupId
            }
            $item = [PSCustomObject]@{
                IsSelected = $true
                Title =  $groupName
                GroupId = $groupId
            }
            $objectList += $item
        }        

        $objectList = $objectList | Sort -Property Title

        $sourceType = "ObjectTypes"
    }
    else
    {
        return
    }

    if($objectList.Count -eq 0) { Write-Log "No objects found/selected!";return }
    $ocList = [System.Collections.ObjectModel.ObservableCollection[object]]::new(@($objectList))

    $script:docForm = Get-XamlObject ($global:AppRootFolder + "\Xaml\DocumentationForm.xaml") -AddVariables
    if(-not $script:docForm) { return }

    $global:grdDocumentObjects.Tag = $sourceType

    if($sourceType -eq "Objects")
    {
        $global:btnClearDocumentationList.Visibility = ?: ($SelectedDocuments -eq $true) "Visible" "Collapsed"
        $global:btnAddToDocumentationList.Visibility = ?: ($SelectedDocuments -ne $true) "Visible" "Collapsed"
    }
    else
    {
        $global:btnClearDocumentationList.Visibility = "Collapsed"
        $global:btnAddToDocumentationList.Visibility = "Collapsed"
    }

    if($ShowFolderSource -ne $true)
    {
        $global:grdDocumentFromFolder.Visibility = "Collapsed"
        $global:spDocumentFromFolder.Visibility = "Collapsed"

        $global:txtDocumentFilter.Visibility = "Collapsed"
        $global:spDocumentFilter.Visibility = "Collapsed"
    }
    else
    {
        Add-XamlEvent $script:docForm "browseDocumentFromFolder" "add_click" {
            $folder = Get-Folder (Get-XamlProperty $script:docForm "txtDocumentFromFolder" "Text") "Select root folder for export files"
            if($folder)
            {
                Set-XamlProperty $script:docForm "txtDocumentFromFolder" "Text" $folder
            }
        }        
    }

    $column = Get-GridCheckboxColumn "IsSelected"
    $global:grdDocumentObjects.Columns.Add($column)

    $column.Header.IsChecked = $true # All items are checked by default
    $column.Header.add_Click({
            foreach($item in $global:grdDocumentObjects.ItemsSource)
            {
                $item.IsSelected = $this.IsChecked
            }
            $global:grdDocumentObjects.Items.Refresh()
        }
    )    

    # Add title column
    $binding = [System.Windows.Data.Binding]::new("Title")
    $column = [System.Windows.Controls.DataGridTextColumn]::new()
    $column.Header = "Title"
    $column.IsReadOnly = $true
    $column.Binding = $binding
    $global:grdDocumentObjects.Columns.Add($column)
    
    if($SelectedDocuments -eq $true)
    {
        $binding = [System.Windows.Data.Binding]::new("ObjectType.Id")
        $column = [System.Windows.Controls.DataGridTextColumn]::new()
        $column.Header = "Type"
        $column.IsReadOnly = $true
        $column.Binding = $binding
        $global:grdDocumentObjects.Columns.Add($column)  
    }

    $global:grdDocumentObjects.ItemsSource = [System.Windows.Data.CollectionViewSource]::GetDefaultView($ocList)    

    Set-FromValues
    
    Add-XamlEvent $script:docForm "btnClose" "add_click" {
        $script:docForm = $null
        Show-ModalObject 
    }

    Add-XamlEvent $script:docForm "btnAddToDocumentationList" "add_click" {
        Add-DocumentationObjects ($global:grdDocumentObjects.ItemsSource | Where IsSelected -eq $true)
    }

    Add-XamlEvent $script:docForm "btnClearDocumentationList" "add_click" {
        if([System.Windows.MessageBox]::Show("Are you sure you want to clear the items in the list?", "Documentation", "YesNo", "Question") -eq "Yes")
        {
            $global:grdDocumentObjects.ItemsSource = $null
            $global:btnStartDocumentation.IsEnabled = $false
        }
    }

    Add-XamlEvent $script:docForm "btnStartDocumentation" "add_click" {

        Invoke-StartDocumentatiom       
    }
    
    Add-XamlEvent $script:docForm "btnCopyBasic" "add_click" {
        if($script:objectBasicInfo.Count -gt 0)
        {
            $script:objectBasicInfo | Select -Property Name,Value | ConvertTo-Csv -NoTypeInformation | Set-Clipboard
        }
    }

    Add-XamlEvent $script:docForm "btnCopySettings" "add_click" {
        if($script:objectSettingsData.Count -gt 0)
        {
            $script:objectSettingsData | Select -Property $script:settingsProperties | ConvertTo-Csv -NoTypeInformation | Set-Clipboard
        }
    }

    Add-XamlEvent $script:docForm "btnCopyAll" "add_click" {
        $tmpArr = @()
        
        if($script:objectBasicInfo.Count -gt 0)
        {
            $tmpArr += $script:objectBasicInfo | Select -Property Name,Value | ConvertTo-Csv -NoTypeInformation 
        }

        if($script:objectSettingsData.Count -gt 0)
        {
            $tmpArr += $script:objectSettingsData | Select -Property $script:settingsProperties | ConvertTo-Csv -NoTypeInformation
        }

        if($script:applicabilityRules.Count -gt 0)
        {
            $tmpArr += $script:applicabilityRules | Select -Property Rule,Property,Value,Category | ConvertTo-Csv -NoTypeInformation
        }                

        if($script:objectComplianceActionData.Count -gt 0)
        {
            $tmpArr += $script:objectComplianceActionData | Select Action,Schedule,MessageTemplate,EmailCC,Category | ConvertTo-Csv -NoTypeInformation
        }
        $tmpArr | Set-Clipboard
    }

    $global:cbDocumentationType.Add_SelectionChanged({        
        Set-OutputOptionsTabStatus $this
    })
    
    if($global:grdDocumentObjects.Tag -eq "ObjectTypes")
    {
        $global:btnExportSettingsForSilentExport.Visibility = "Visible"

        Add-XamlEvent $script:docForm "btnExportSettingsForSilentExport" "add_click" ({
            $sf = [System.Windows.Forms.SaveFileDialog]::new()
            $sf.FileName = "BulkDocumentation.json"
            $sf.DefaultExt = "*.json"
            $sf.Filter = "Json (*.json)|*.json|All files (*.*)|*.*"
            if($sf.ShowDialog() -eq "OK")
            {
                $tmp = [PSCustomObject]@{
                    Name = "ObjectTypes"
                    Type = "Custom"
                    ObjectTypes = @()
                }

                foreach($tmpObj in ($global:grdDocumentObjects.ItemsSource | Where IsSelected -eq $true))
                {
                    $tmp.ObjectTypes += $tmpObj.GroupId
                }
                Export-GraphBatchSettings $sf.FileName $script:docForm "BulkDocumentation" @($tmp)
            }  
        })
    }

    Set-OutputOptionsTabStatus $global:cbDocumentationType

    Show-ModalForm "Intune Documentation" $script:docForm -HideButtons
}

function Invoke-SilentBatchJob
{
    param($settingsObj)

    if($settingsObj.BulkDocumentation)
    {
        $global:currentViewObject =  $global:viewObjects | Where { $_.ViewInfo.ID -eq "IntuneGraphAPI" }
        Start-DocSilentBulkDocumentation $settingsObj
    }
}

function Start-DocSilentBulkDocumentation
{
    param($settingsObj)

    $script:docForm = Get-XamlObject ($global:AppRootFolder + "\Xaml\DocumentationForm.xaml") -AddVariables
    if(-not $script:docForm) { return }
        
    $global:grdDocumentObjects.Tag = "ObjectTypes"

    # Get all objects but not selected
    # This will allow dependencies
    $script:docObjectTypes = Get-GraphBatchObjectTypes $settingsObj.BulkDocumentation -NotSelected -All
    
    $objectList = @()
    $objTypes = $settingsObj.BulkDocumentation | Where Name -eq ObjectTypes
    if($objTypes)
    {        
        # Select object types from the batch file        
        foreach($objTypeId in $objTypes.ObjectTypes)
        {
            $objectList += [PSCustomObject]@{
                IsSelected = $true
                Title = $objTypeId
                GroupId = $objTypeId
            }
        }
    }    

    if($objectList.Count -eq 0) { Write-Log "No objects found/selected!";return }
    $ocList = [System.Collections.ObjectModel.ObservableCollection[object]]::new(@($objectList)) 

    $global:grdDocumentObjects.ItemsSource = [System.Windows.Data.CollectionViewSource]::GetDefaultView($ocList)    

    Set-FromValues

    $global:cbDocumentationType.SelectedValue = ($settingsObj.BulkDocumentation | Where Name -eq "cbDocumentationType").value

    Set-OutputOptionsTabStatus $global:cbDocumentationType    
    
    # Skip warning since it can't find custom tab controls in main from (eg Word settings)
    # Probably requires some RegisterName...
    # ToDo: Fix this in a proper way
    Set-BatchProperties $settingsObj.BulkDocumentation $script:docForm -SkipMissingControlWarning
    Set-BatchProperties $settingsObj.BulkDocumentation $global:ccOutputCustomOptions.Content -SkipMissingControlWarning

    Invoke-StartDocumentatiom
}

function local:Set-FromValues
{
    $global:cbDocumentationType.ItemsSource = $global:documentationOutput
    $global:cbDocumentationType.SelectedValue = (Get-Setting "Documentation" "OutputType" "none")

    if(-not $script:Languages)
    {
        $script:Languages = Get-Content ($global:AppRootFolder + "\Documentation\Languages.json") -Encoding UTF8 | ConvertFrom-Json
    }

    if($script:Languages)
    {
        $global:cbDocumentationLanguage.ItemsSource = $script:Languages
        $global:cbDocumentationLanguage.SelectedValue = (Get-Setting "Documentation" "Language" "en")
    }

    $global:cbDocumentationPropertySeparator.ItemsSource = @(",",";","-","|")
    try
    {
        $global:cbDocumentationPropertySeparator.SelectedIndex = $global:cbDocumentationPropertySeparator.ItemsSource.IndexOf((Get-Setting "Documentation" "PropertySeparator" ";"))
    }
    catch {}

    $objectSeparator = "[ { Name: `"New line`",Value: `"$([System.Environment]::NewLine)`" }, {Name: `";`",Value: `";`" }, {Name: `"|`",Value: `"|`" }]" | ConvertFrom-Json
    $global:cbDocumentationObjectSeparator.ItemsSource = $objectSeparator
    $global:cbDocumentationObjectSeparator.SelectedValue = (Get-Setting "Documentation" "ObjectSeparator" ([System.Environment]::NewLine)) #"$([System.Environment]::NewLine)")

    $valueOutputPropertiyTypes = "[ { Name: `"Value`",Value: `"value`" }, {Name: `"Value with label`", Value: `"valueWithLabel`" }]" | ConvertFrom-Json
    $global:cbDocumentationValueOutputProperty.ItemsSource = $valueOutputPropertiyTypes
    $global:cbDocumentationValueOutputProperty.SelectedValue = (Get-Setting "Documentation" "ValueOutputProperty" "value")

    $global:chkSetUnconfiguredValue.IsChecked = ((Get-Setting "Documentation" "SetUnconfiguredValue" "true") -ne "false")
    $global:chkSetDefaultValue.IsChecked = ((Get-Setting "Documentation" "SetDefaultValue" "false") -ne "false")

    $global:chkIncludeScripts.IsChecked = ((Get-Setting "Documentation" "IncludeScripts" "true") -ne "false")
    $global:chkExcludeScriptSignature.IsChecked = ((Get-Setting "Documentation" "ExcludeScriptSignature" "false") -ne "false")
    $global:chkExcludeAssignments.IsChecked = ((Get-Setting "Documentation" "ExcludeAssignments" "false") -eq "true")
    
    $notConfiguredItems = "[ { Name: `"Not configured (Localized)`",Value: `"notConfigured`" }, { Name: `"Empty`",Value: `"empty`" }, { Name: `"Don't change`",Value: `"asis`" }]" | ConvertFrom-Json
    $global:cbNotConifugredText.ItemsSource = $notConfiguredItems
    $global:cbNotConifugredText.SelectedValue = (Get-Setting "Documentation" "NotConfiguredText" "")

    $global:chkSkipNotConfigured.IsChecked = ((Get-Setting "Documentation" "SkipNotConfigured" "false") -ne "false")
    $global:chkSkipDefaultValues.IsChecked = ((Get-Setting "Documentation" "SkipDefaultValues" "false") -ne "false")
    $global:chkSkipDisabled.IsChecked = ((Get-Setting "Documentation" "SkipDisabled" "true") -ne "false")    
}
function local:Invoke-StartDocumentatiom
{
    $txtDocumentationRawData.Text = ""

    $script:offlineDocumentation = $false
    $script:offlineObjects = @{}

    $loadExportedInfo = $true
    $script:migTable = $null
    $script:scopeTags = $null

    $diSource = $nul
    $global:intentCategories = $null
    $global:catRecommendedSettings = $null
    $global:intentCategoryDefs = $null
    $global:cfgCategories = $null

    $script:DocumentationLanguage = ?? $global:cbDocumentationLanguage.SelectedValue "en"        
    $script:objectSeparator = ?? $global:cbDocumentationObjectSeparator.SelectedValue ([System.Environment]::NewLine)
    $script:propertySeparator = ?? $global:cbDocumentationPropertySeparator.SelectedValue ","

    $script:ValueOutputProperty = ?? $global:cbDocumentationValueOutputProperty.SelectedValue "value"  

    Save-Setting "Documentation" "OutputType" $global:cbDocumentationType.SelectedValue        

    Save-Setting "Documentation" "Language" $script:DocumentationLanguage
    Save-Setting "Documentation" "ObjectSeparator" $script:objectSeparator
    Save-Setting "Documentation" "PropertySeparator" $script:propertySeparator

    Save-Setting "Documentation" "ValueOutputProperty" $script:ValueOutputProperty

    Save-Setting "Documentation" "SetUnconfiguredValue" $global:chkSetUnconfiguredValue.IsChecked
    Save-Setting "Documentation" "SetDefaultValue" $global:chkSetDefaultValue.IsChecked

    Save-Setting "Documentation" "IncludeScripts" $global:chkIncludeScripts.IsChecked
    Save-Setting "Documentation" "ExcludeScriptSignature" $global:chkExcludeScriptSignature.IsChecked
    Save-Setting "Documentation" "ExcludeAssignments" $global:chkExcludeAssignments.IsChecked    

    Save-Setting "Documentation" "SkipNotConfigured" $global:chkSkipNotConfigured.IsChecked
    Save-Setting "Documentation" "SkipDefaultValues" $global:chkSkipDefaultValues.IsChecked
    Save-Setting "Documentation" "SkipDisabled" $global:chkSkipDisabled.IsChecked

    Save-Setting "Documentation" "NotConfiguredText" $global:cbNotConifugredText.SelectedValue

    Get-CustomIgnoredCategories $obj

    Invoke-InitDocumentation

    if($global:grdDocumentObjects.Tag -eq "Objects")
    {
        $sourceList = @()
        $groupIds = $global:grdDocumentObjects.ItemsSource.ObjectType.GroupId | Select -Unique | Sort GroupId
        foreach($groupId in $groupIds)
        {
            $groupSourceList = @()
            $curObjectType = $null 
            foreach($tmpObj in ($global:grdDocumentObjects.ItemsSource | Where { $_.IsSelected -eq $true -and $_.ObjectType.GroupId -eq $groupId } ))
            {
                $groupSourceList += $tmpObj
                $curObjectType = $tmpObj.ObjectType
                if($curObjectType.GroupId -eq "EndpointSecurity")
                {
                    $catName = Get-IntentCategory $tmpObj.Category
                    $tmpObj | Add-Member Noteproperty -Name "CategoryName" -Value $catName -Force
                }
                else
                {
                    $tmpObj | Add-Member Noteproperty -Name "Category" -Value $tmpObj.ObjectType.Id -Force
                }

                $tmpObj | Add-Member Noteproperty -Name "GroupName" -Value (Get-ObjectTypeGroupName $tmpObj.ObjectType) -Force
            }

            if($groupSourceList.Count -eq 0) { contnue }

            if($curObjectType.GroupId -eq "EndpointSecurity")
            {
                $sortProps = @("CategoryName","Title") # "displayName")
            }
            else
            {
                #!!!###$sortProps =  @({$_.ObjectType.Title},{(Get-GraphObjectName $_.Object $_.ObjectType)}) #@("Title") #@((?? $objectType.NameProperty "displayName"))
                $sortProps = @({$_.GroupName},{(Get-GraphObjectName $_.Object $_.ObjectType)}) 
            }
            $sourceList += ($groupSourceList | Sort-Object -Property $sortProps)
        }
    }
    elseif($global:grdDocumentObjects.Tag -eq "ObjectTypes")
    {
        $fromExportFolder = $false
        if($global:txtDocumentFromFolder.Text)
        {
            $diSource = [IO.DirectoryInfo]$global:txtDocumentFromFolder.Text
            if($diSource.Exists -eq $false)
            {
                [System.Windows.MessageBox]::Show("Source folder not found:`n`n$($diSource.FullName)", "Documentation", "OK", "Error")
                Write-Status ""
                return
            }
            $fromExportFolder = $true
        }

        $sourceList = @()
        foreach($objGroup in ($global:grdDocumentObjects.ItemsSource | Where IsSelected -eq $true))
        {
            $groupSourceList = @()
            foreach($objectType in ($global:currentViewObject.ViewItems | Where GroupId -eq $objGroup.GroupId))
            {
                Write-Status "Get $($objectType.Title) objects"
            
                if($fromExportFolder -eq $false)
                {
                    [array]$graphObjects = Get-GraphObjects -property $objectType.ViewProperties -objectType $objectType
                }
                else
                {
                    $objectPath = [IO.Path]::Combine($diSource.FullName,$objectType.ID)
                    if([IO.Directory]::Exists($objectPath) -eq $false)
                    {
                        Write-Log "Object path for $($objectType.Title) ($($objectType.ID)) not found. Skipping object type" 2 
                        continue
                    }
                    $graphObjects = Get-GraphFileObjects $objectPath -ObjectType $objectType
                    $graphObjects | ForEach-Object { $_.Object | Add-Member Noteproperty -Name "@ObjectFromFile" -Value $true -Force }
                }
                $groupSourceList += $graphObjects
            }

            if($objGroup.GroupId -eq "EndpointSecurity")
            {
                foreach($tmpObj in $groupSourceList)
                {
                    $catName = Get-IntentCategory $tmpObj.Category
                    $tmpObj | Add-Member Noteproperty -Name "CategoryName" -Value $catName -Force
                }
                $sortProps = @("CategoryName","displayName")
            }
            else
            {
                foreach($tmpObj in $groupSourceList)
                {
                    $tmpObj | Add-Member Noteproperty -Name "Category" -Value $tmpObj.ObjectType.Id -Force
                    $tmpObj | Add-Member Noteproperty -Name "GroupName" -Value (Get-ObjectTypeGroupName $tmpObj.ObjectType) -Force
                }                
                #!!!###$sortProps = @({$_.ObjectType.Title},{(Get-GraphObjectName $_.Object $_.ObjectType)}) 
                $sortProps = @({$_.GroupName},{(Get-GraphObjectName $_.Object $_.ObjectType)}) 
            }
            $sourceList += ($groupSourceList | Sort-Object -Property $sortProps)
        }
    }
    else
    {
        return  
    }

    if($fromExportFolder -eq $true -and $diSource -and $loadExportedInfo -eq $true)
    {
        $loadExportedInfo = $false

        $migFileName = [IO.Path]::Combine($diSource.FullName,"MigrationTable.json")
        if([IO.File]::Exists($migFileName) -eq $false)
        {
            Write-Log "MigrationTable not found. Groups will be documented with GroupId" 2
        }
        else
        {
            Write-Log "Load Migration table from $migFileName"
            $script:migTable = ConvertFrom-Json (Get-Content $migFileName -Raw)
        }

        if($script:migTable.TenantId -and $script:migTable.TenantId -ne $global:organization.id)
        {
            $script:offlineDocumentation = $true
        }

        if($script:offlineDocumentation -eq $true)
        {
            Add-DocOfflineDependencies "ScopeTags" $diSource.FullName
            Add-DocOfflineDependencies "AssignmentFilters" $diSource.FullName
            Add-DocOfflineObjectTypeDependencies  $diSource.FullName
            if($script:offlineObjects.ContainsKey("ScopeTags"))
            {
                $script:scopeTags = @($script:offlineObjects["ScopeTags"])
            }                
        }
    }

    if($global:cbDocumentationType.SelectedItem.PreProcess)
    {
        Write-Status "Run PreProcess for $($global:cbDocumentationType.SelectedItem.Name)"
        & $global:cbDocumentationType.SelectedItem.PreProcess
    }

    $tmpCurObjectType = $null
    $tmpCurObjectGroup = $null
    $allObjectTypeObjects = @()

    $groupCategoryCount = 2 # Force group header. The above code is not working since it should be inside the actual group

    $filter = $global:txtDocumentFilter.Text.Trim()
    
    Write-Log "Filter: $filter" 

    $tmpList = @()
    
    # Remove objects that should be excluded based on filter
    # Get documentation data and remove objects not supporting documentation
    foreach($curObj in $sourceList)
    {
        # Filter out objects if they return scopeTags in a list
        if($curObj.ObjectType.ScopeTagsReturnedInList -ne $false -and (Confirm-GraphMatchFilter $curObj $filter) -eq $false)
        {
            continue
        }
        
        if($curObj.Object."@ObjectFromFile" -eq $true)
        {
            $obj = $curObj
        }
        else
        {
            $obj = Get-GraphObject $curObj.Object $curObj.ObjectType
            $curObj.Object = $obj.Object 
        }

        # Second check for object types that don't return scopeTag in list
        if($curObj.ObjectType.ScopeTagsReturnedInList -eq $false -and (Confirm-GraphMatchFilter $obj $filter) -eq $false)
        {        
            continue
        }

        $documentedObj = Get-ObjectDocumentation $curObj
        if($documentedObj.ErrorText)
        {
            if($txtDocumentationRawData.Text)
            {
                $txtDocumentationRawData.Text += "`n`n"    
            }

            $txtDocumentationRawData.Text += "#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" 
            $txtDocumentationRawData.Text += "`n#`n# Object: $((Get-GraphObjectName $curObj.Object $curObj.ObjectType))" 
            $txtDocumentationRawData.Text += "`n# Type: $($curObj.Object.'@OData.Type')" 
            $txtDocumentationRawData.Text += "`n#`n# Object not documented. Error:"
            $txtDocumentationRawData.Text += "`n# $(($documentedObj.ErrorText -replace "`n","`n# "))"
            $txtDocumentationRawData.Text += "`n#`n#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" 
            continue
        }
        elseif(-not $documentedObj)
        {
            continue
        }

        $curObj | Add-Member Noteproperty -Name "Documentation" -Value $documentedObj -Force 

        $tmpList += $curObj
    }
    $sourceList = $tmpList
    
    # Add each object to the documentation
    foreach($curGroupId in ($sourceList.ObjectType | Select GroupID -Unique).GroupID)
    {
        # New object group e.g. Script, Tenant, Device Configuration
        # A group matches a menu item in the protal but can contain multiple object types
        if($global:cbDocumentationType.SelectedItem.NewObjectGroup)
        {
            Write-Status "Run NewObjectGroup for $($global:cbDocumentationType.SelectedItem.Name)"
            $ret = & $global:cbDocumentationType.SelectedItem.NewObjectGroup $curGroupId
            if($ret -is [boolean] -and $ret -eq $true) { continue }
        }

        foreach($curCategory in ($sourceList | Where { $_.ObjectType.GroupID -eq $curGroupId } | Select Category -Unique).Category)
        {
            $obj = $sourceList | Where { $_.Category -eq $curCategory } | Select -First 1
            if(-not $obj) { continue }

            # New object type e.g Administrative Template, VPN profile etc.
            if($global:cbDocumentationType.SelectedItem.NewObjectType)
            {
                if($obj.ObjectType.GroupId -eq "EndpointSecurity")
                {
                    $objectTypeString = $obj.CategoryName
                }
                else
                {
                    $objectTypeString = $obj.GroupName #!!!###$obj.ObjectType.Title
                }
                $objectTypeString = (?? $objectTypeString $obj.ObjectType.Title)

                Write-Status "Run NewObjectType for $($global:cbDocumentationType.SelectedItem.Name)"
                $ret = & $global:cbDocumentationType.SelectedItem.NewObjectType $objectTypeString
                if($ret -is [boolean] -and $ret -eq $true) { continue }
            }

            #ObjectType
            foreach($tmpObj in ($sourceList | Where { $_.ObjectType.GroupID -eq $curGroupId -and $_.Category -eq $curCategory}))
            {                
                $obj = $tmpObj
                $documentedObj = $obj.Documentation
                
                if($global:cbDocumentationType.SelectedItem.CustomProcess)
                {
                    # The provider takes care of all the processing
                    Write-Status "Run CustomProcess for $($global:cbDocumentationType.SelectedItem.Name)"
                    $ret = & $global:cbDocumentationType.SelectedItem.CustomProcess $obj $documentedObj
                    if($ret -is [boolean] -and $ret -eq $true) { continue }
                }

                if($documentedObj) 
                {
                    Add-RawDataInfo $obj.Object $obj.ObjectType
    
                    $updateNotConfigured = $true
                    $notConfiguredLoc = Get-LanguageString "BooleanActions.notConfigured"
                    $notConfiguredText = ""
                    if($global:cbNotConifugredText.SelectedValue -eq "notConfigured")
                    {
                        $notConfiguredText = $notConfiguredLoc
                    }
                    elseif($global:cbNotConifugredText.SelectedValue -eq "asis")
                    {
                        $updateNotConfigured = $false 
                    }
                    
                    if($global:cbDocumentationType.SelectedItem.Process)
                    {
                        Write-Status "Process $((Get-GraphObjectName $tmpObj.Object $tmpObj.ObjectType)) ($($obj.ObjectType.Title)) - $($global:cbDocumentationType.SelectedItem.Name)"
    
                        $hasRawValue = $false
                        $documentedObj.Settings | % { if(($_.PSObject.Properties | Where Name -eq "RawValue")) { $hasRawValue=$true } }
                        $hasRawValue
                        
                        $filteredSettings = @()
                        foreach($item in $documentedObj.Settings)
                        {
                            if(-not ($item.PSObject.Properties | Where Name -eq "RawValue") -or $documentedObj.UpdateFilteredObject -eq $false)
                            {
                                if($hasRawValue -eq $false) { 
                                    $filteredSettings = $documentedObj.Settings 
                                    break
                                }
                                $filteredSettings += $item
                                continue
                            }
                            
                            if($item.AlwaysAddValue -eq $true)
                            {
                                
                            }
                            elseif($global:chkSkipNotConfigured.IsChecked -and (($item.RawValue -isnot [array] -and ([String]::IsNullOrEmpty($item.RawValue) -or ("$($item.RawValue)" -eq "notConfigured"))) -or ($item.RawValue -is [array] -and ($item.RawValue | measure).Count -eq 0)))
                            {
                                # Skip unconfigured items e.g. properties with null values
                                # Note: This could removed configured properties if RawValue is not specified
                                continue                
                            }
                            elseif($global:chkSkipNotConfigured.IsChecked -and $documentedObj.UnconfiguredProperties -and ($documentedObj.UnconfiguredProperties | Where EntityKey -eq $item.EntityKey))
                            {
                                # Skip unconfigured items e.g. boolean with a value but Not Configured
                                continue                
                            }
    
                            if($global:chkSkipDefaultValues.IsChecked -and (($item.DefaultValue -and $item.RawValue -eq $item.DefaultValue) -or ($item.UnconfiguredValue -and $item.RawValue -eq $item.UnconfiguredValue)))
                            {
                                # Skip items that is using default or unconfiguered values                                                               
                                continue                
                            }                            
    
                            if($global:chkSkipDisabled.IsChecked -and ($item.Enabled -is [Boolean] -and ($item.Enabled -eq $false)))
                            {
                                # Skip Disabled items
                                continue
                            }
                            elseif($item.EntityKey -and ($item.Enabled -is [Boolean] -and ($item.Enabled -eq $false)))
                            {
                                if(($documentedObj.Settings | Where { $_.EntityKey -eq $item.EntityKey -and $_.Enabled -eq $true }))
                                {
                                    # Skip a disabled item if there is another item with the same property that is enabled
                                    continue
                                }
                            }
    
                            if($updateNotConfigured -and (($item.RawValue -isnot [array] -and ($null -eq $item.RawValue -or "$($item.RawValue)" -eq "" -or "$($item.RawValue)" -eq "notConfigured") -and [String]::IsNullOrEmpty($item.Value)) -or ($item.RawValue -is [array] -and ($item.RawValue | measure).Count -eq 0)))
                            {
                                $item.Value = $notConfiguredText
                            }
                            
                            if($global:chkSkipNotConfigured.IsChecked -and $item.Value -eq $notConfiguredLoc)
                            {
                                # Skip unconfigured items based on value e.g. value = Not Configured 
                                Write-Log "Skipping property $($item.Name) based on '$($notConfiguredLoc)' string value" 2
                                continue
                            }
    
                            $filteredSettings += $item
                        }
    
                        $documentedObj | Add-Member Noteproperty -Name "FilteredSettings" -Value $filteredSettings -Force 

                        & $global:cbDocumentationType.SelectedItem.Process $obj.Object $obj.ObjectType $documentedObj
                    }
                }
            }
        }
    }
    
    if($global:cbDocumentationType.SelectedItem.PostProcess)
    {
        Write-Status "Run PostProcess for $($global:cbDocumentationType.SelectedItem.Name)"
        & $global:cbDocumentationType.SelectedItem.PostProcess
    }

    if($script:offlineDocumentation -eq $true)
    {
        # Clear the dependency objects loaded for Offline documentation
        $global:LoadedDependencyObjects = $null
    }
    $script:offlineDocumentation = $false
    Write-Status ""         
}

function Confirm-MatchFilter
{
    param($graphObj, [string]$filter)

    if(-not $filter.Trim()) { return $true }

    $filterScope = ""

    if($filter -like "scope:*" -or $filter -like "tag:*")
    {
        $filterScope = $filter.Split(':')[1]
    }

    $objName = Get-GraphObjectName $graphObj.Object $graphObj.ObjectType
    if($filterScope)
    {
        if(($graphObj.Object.PSObject.Properties | Where Name -eq "roleScopeTagIds"))
        {
            $scopeTagProperty = "roleScopeTagIds"
        }
        elseif(($graphObj.Object.PSObject.Properties | Where Name -eq "roleScopeTags"))
        {
            $scopeTagProperty = "roleScopeTags"
        }
        else
        {
            Write-Log "$objName excluded based on Scope(Tags) not supported on $($graphObj.ObjectType.GroupId) objects"
            continue
        }

        if(-not $script:scopeTags -and $script:offlineDocumentation -ne $true)
        {
            $script:scopeTags = (Invoke-GraphRequest -Url "/deviceManagement/roleScopeTags").Value
        }
        
        $found = $false
        foreach($scopeTagId in $graphObj.Object."$scopeTagProperty")
        {                    
            $scopeTagObj = $script:scopeTags | Where Id -eq $scopeTagId
            if($scopeTagObj -and $filterScope -and $scopeTagObj.displayName -match [RegEx]::Escape($filterScope))
            {
                return $true               
            }
        }

        if($found -eq $false)
        {
            Write-Log "$objName excluded based on no Scope(Tags) found that matches the filter"
            return $false
        }
    }
    else
    {                
        if($objName -and $filter -and $objName -notmatch [RegEx]::Escape($filter))
        {
            Write-Log "$objName excluded based on the name does not match the filter"
            return $false
        }
    }
    return $true
}
    
function Get-DocOfflineObjects
{
    param($objectName)

    if($script:offlineDocumentation -eq $false) { return }

    if($script:offlineObjects.ContainsKey($objectName))
    {
        $script:offlineObjects[$objectName]
    }
}

function Set-OutputOptionsTabStatus
{
    param($control)

    $global:tabOutputSettings.Visibility = (?: ($control.SelectedItem.Value -eq "none") "Collapsed" "Visible")

    $global:lblCustomOptions.Content = ?: $control.SelectedItem.OutputOptions ($control.SelectedItem.Name) ""
    $global:ccOutputCustomOptions.Content = $control.SelectedItem.OutputOptions

    if($control.SelectedItem.Activate)
    {
        & $control.SelectedItem.Activate
    }
}

function Invoke-DocumentObjectTypes
{
   Show-DocumentationForm -objectTypes $global:currentViewObject.ViewItems -ShowFolderSource
}

function Invoke-DocumentSelectedObjects
{
    if(-not $script:selectedObjects -or $script:selectedObjects.Count -eq 0)
    {
        [System.Windows.MessageBox]::Show("No objects added for documentation", "Documentation", "OK", "Info")
        return
    }

    Show-DocumentationForm -objects $script:selectedObjects -SelectedDocuments
}

function Add-DocOfflineObjectTypeDependencies 
{
    param($fromFolder)

    foreacH($viewItem in $global:currentViewObject.ViewItems)
    {
        foreach($dep in $viewItem.Dependencies)
        {
            Add-DocOfflineDependencies $dep $fromFolder
        }
    }
}

function Add-DocOfflineDependencies 
{
    param($objectTypeName, $fromFolder)

    if($script:offlineObjects.ContainsKey($objectTypeName)) { return }

    $tmpObjType = $global:currentViewObject.ViewItems | Where Id -eq $objectTypeName

    if($tmpObjType)
    {
        $objPath = [IO.Path]::Combine($fromFolder,$tmpObjType.Id)
        if([IO.Directory]::Exists($objPath) -eq $false)
        {
            Write-Log "Object path for $($tmpObjType.Title) ($($objPath)) not found" 2                 
        }
        else
        {                    
            $tmpObjects = Get-GraphFileObjects $objPath -ObjectType $tmpObjType
            $script:offlineObjects.Add($tmpObjType.Id, @(($tmpObjects | Select Object).Object))
        }
    }
}

function Add-DocumentationObjects
{
    param($objects)

    $itemCount = ($objects | measure).Count

    if($itemCount -eq 0) { return }

    if(-not $script:selectedObjects)
    {
        $script:selectedObjects = @()
    }
    $script:selectedObjects += $objects

    [System.Windows.MessageBox]::Show("$itemCount object(s) added to the documentation list", "Documentation", "OK", "Info")

}

function Clear-DocumentationObjects
{
    $script:selectedObjects = @()
}

#endregion

#region CSV Options

function Add-CSVOptionsControl
{
    $script:csvForm = Get-XamlObject ($global:AppRootFolder + "\Xaml\DocumentationCSVOptions.xaml") -AddVariables

    $global:cbCSVDocumentationProperties.ItemsSource = ("[ { Name: `"Simple (Name and Value)`",Value: `"simple`" }, { Name: `"Extended`",Value: `"extended`" }, { Name: `"Custom`",Value: `"custom`" }]" | ConvertFrom-Json)
    $global:cbCSVDocumentationProperties.SelectedValue = (Get-Setting "Documentation" "CSVExportProperties" "simple")
    $global:txtCSVCustomProperties.Text = (Get-Setting "Documentation" "CSVCustomDisplayProperties" "Name,Value,Category")

    $global:spCSVCustomProperties.Visibility = (?: ($global:cbCSVDocumentationProperties.SelectedValue -ne "custom") "Collapsed" "Visible")
    $global:txtCSVCustomProperties.Visibility = (?: ($global:cbCSVDocumentationProperties.SelectedValue -ne "custom") "Collapsed" "Visible")

    $global:cbCSVDelimiter.ItemsSource = @("", ",",";","-","|")
    try
    {
        $global:cbCSVDelimiter.SelectedIndex = $global:cbCSVDelimiter.ItemsSource.IndexOf((Get-Setting "Documentation" "CSVDelimiter"))
    }
    catch {}

    Add-XamlEvent $script:csvForm "browseCSVDocumentationPath" "add_click" {
        $folder = Get-Folder (Get-XamlProperty $script:csvForm "txtCSVDocumentationPath" "Text") "Select root folder for export"
        if($folder)
        {
            Set-XamlProperty $script:csvForm "txtCSVDocumentationPath" "Text" $folder
        }
    }
    
    Add-XamlEvent $script:csvForm "cbCSVDocumentationProperties" "add_selectionChanged" {
        $global:spCSVCustomProperties.Visibility = (?: ($this.SelectedValue -ne "custom") "Collapsed" "Visible")
        $global:txtCSVCustomProperties.Visibility = (?: ($this.SelectedValue -ne "custom") "Collapsed" "Visible")
    }

    $script:csvForm
}
function Invoke-CSVActivate
{
    $global:txtCSVDocumentationPath.Text = (?? (Get-Setting "" "LastUsedRoot") (Get-SettingValue "RootFolder"))
    $global:chkCSVAddObjectType.IsChecked = (Get-SettingValue "AddObjectType")
    $global:chkCSVAddCompanyName.IsChecked = (Get-SettingValue "AddCompanyName")
}

function Invoke-CSVPreProcessItems
{
    Save-Setting "Documentation" "CSVExportProperties" $global:cbCSVDocumentationProperties.SelectedValue
    Save-Setting "Documentation" "CSVCustomDisplayProperties" $global:txtCSVCustomProperties.Text
    Save-Setting "Documentation" "CSVDelimiter" $global:cbCSVDelimiter.Text
}

function Invoke-CSVProcessItem
{
    param($obj, $objectType, $documentedObj)

    if(!$documentedObj -or !$obj -or !$objectType) { return }

    $folder = Get-GraphObjectFolder $objectType (Get-XamlProperty $script:csvForm "txtCSVDocumentationPath" "Text") (Get-XamlProperty $script:csvForm "chkCSVAddObjectType" "IsChecked") (Get-XamlProperty $script:csvForm "chkCSVAddCompanyName" "IsChecked")

    try 
    {
        if([IO.Directory]::Exists($folder) -eq $false)
        {
            [IO.Directory]::CreateDirectory($folder)
        }

        $objName = Get-GraphObjectName $obj $objectType

        #BasicInfo
        #Settings
        #ComplianceActions
        #Assignments
        #DisplayProperties

        $itemsToExport = @()
        
        $params = @{}
        if($global:cbCSVDelimiter.Text)
        {
            $params.Add('Delimiter',$global:cbCSVDelimiter.Text)
        }

        if(($global:cbCSVDocumentationProperties.SelectedValue -eq 'extended' -and $documentedObj.DisplayProperties) -or 
            ($global:cbCSVDocumentationProperties.SelectedValue -eq 'custom' -and $global:txtCSVCustomProperties.Text))
        {
            if(($documentedObj.BasicInfo | measure).Count -gt 0)
            {
                $itemsToExport += ""
                $itemsToExport += "# Basic info"
                $itemsToExport += ""
                $itemsToExport += $documentedObj.BasicInfo | ConvertTo-Csv -NoTypeInformation
            }

            if(($documentedObj.FilteredSettings | measure).Count -gt 0)
            {
                $itemsToExport += ""
                $itemsToExport += "# Settings"
                $itemsToExport += ""
                if($global:cbCSVDocumentationProperties.SelectedValue -eq 'extended')
                {
                    $displayProperties = $documentedObj.DisplayProperties
                }
                else
                {
                    $displayProperties = $global:txtCSVCustomProperties.Text.Split(",")
                }
                $itemsToExport += $documentedObj.FilteredSettings | Select $displayProperties | ConvertTo-Csv -NoTypeInformation
            }

            if(($documentedObj.ApplicabilityRules | measure).Count -gt 0)
            {
                $itemsToExport += ""
                $itemsToExport += "# Applicability Rules"
                $itemsToExport += ""
                $itemsToExport += $script:applicabilityRules | Select Rule,Property,Value,Category | ConvertTo-Csv -NoTypeInformation
            }

            if(($documentedObj.ComplianceActions | measure).Count -gt 0)
            {
                $itemsToExport += ""
                $itemsToExport += "# Compliance Actions"
                $itemsToExport += ""
                $itemsToExport += $documentedObj.ComplianceActions | Select Action,Schedule,MessageTemplate,EmailCC,Category | ConvertTo-Csv -NoTypeInformation
            }

            if(($documentedObj.Assignments | measure).Count -gt 0)
            {
                if($documentedObj.Assignments[0].RawIntent)
                {
                    $properties = @("GroupMode","Group","Category","SubCategory")
                }
                elseif($documentedObj.Assignments[0].Group)
                {
                    $properties = @("GroupMode","Group","Category")
                }
                else
                {
                    $properties = @("GroupMode","Groups","Category")
                }

                $itemsToExport += ""
                $itemsToExport += "# Assignments"
                $itemsToExport += ""
                $itemsToExport += $documentedObj.Assignments  | Select $properties | ConvertTo-Csv -NoTypeInformation @params
            }
        }        
        else
        {
            $itemsToExport += $documentedObj.BasicInfo
            $itemsToExport += $documentedObj.FilteredSettings
            $itemsToExport = $itemsToExport | Select Name,Value | ConvertTo-Csv -NoTypeInformation @params
        }

        $fileName = $folder + "\$((Remove-InvalidFileNameChars $objName)).csv"
        Write-Log "Save documentation to $fileName"
        $itemsToExport | Out-File -LiteralPath $fileName -Encoding UTF8 -Force
    }
    catch 
    {
        Write-LogError "Failed to save CSV file $(($folder + "\$($objName).csv"))" $_.Exception
    }
}
#endregion

#region Invoke-ScopeTags
function Get-ScopeTagsItems
{
    param($documentationObjects, $objectType)

    if((($documentationObjects | Where { $_.Object.ObjectType.Id -eq "ScopeTags" }) | measure).Count -gt 0)
    {
        $items = @()

        $nameLabel = Get-LanguageString "Inputs.displayNameLabel"
        $descriptionLabel = Get-LanguageString "TableHeaders.description" 
        $assignmentsLabel = Get-LanguageString "TableHeaders.assignments"
        
        foreach($fullObj in ($documentationObjects | Where { $_.Object.ObjectType.Id -eq "ScopeTags" }))
        {
            $obj = $fullObj.Object

            $groupIDs, $groupInfo, $filterIds,$filtersInfo = Get-ObjectAssignments $obj

            $items += [PSCustomObject]@{
                $nameLabel = $obj.displayName
                ID = $obj.Id
                $descriptionLabel = $obj.Description
                $assignmentsLabel = ($groupInfo.displayName -join $script:objectSeparator)
            }
        }

        $items = $items | Sort -Property $nameLabel

        $documentationInfo = [PSCustomObject]@{
            TypeName = (Get-LanguageString "SettingDetails.scopeTags")
            ObjectType = $objectType
            Properties = @($nameLabel,"id", $descriptionLable, $assignmentsLabel)
            Items = $items
        }

        return $documentationInfo
    }
}
#endregion

function Get-ObjectAssignments
{
    param($obj)

    $groupIds = @()
    $groupInfo = @()
    $filterIds = @()

    foreach($assignment in $obj.assignments)
    {
        if($assignment.target.groupId -and $assignment.target.groupId -notin $groupIds)
        {
            $groupIds += $assignment.target.groupId
        }

        if($assignment.target.deviceAndAppManagementAssignmentFilterId -and ($assignment.target.deviceAndAppManagementAssignmentFilterId -notin $filterIds))
        {
            $filterIds += $assignment.target.deviceAndAppManagementAssignmentFilterId
        }
    }

    $groupInfo = $null

    if($groupIds.Count -gt 0 -and $script:offlineDocumentation -ne $true)
    {
        $ht = @{}
        $ht.Add("ids", @($groupIds | Unique))

        $body = $ht | ConvertTo-Json

        $groupInfo = (Invoke-GraphRequest -Url "/directoryObjects/getByIds?`$select=displayName,id" -Content $body -Method "Post").Value
    }
    
    if(($null -eq $groupInfo -or ($groupInfo | measure).Count -eq 0) -and $obj."@ObjectFromFile" -eq $true -and $script:migTable)
    {
        ### Get group info from mig table when documenting from file if there's no access to the environment
        $groupInfo = $script:migTable.Objects | Where Type -eq "Group" 
    }

    if($filterIds.Count -gt 0)
    {        
        if($script:offlineDocumentation -eq $true)
        {
            if($script:offlineObjects["AssignmentFilters"])
            {
                $filtersInfo = $script:offlineObjects["AssignmentFilters"] | Where { $_.Id -in $filterIds }
            }
            else
            {
                Write-Log "No assignment filters loaded for Offline documentation. Check export folder" 2
            }
        }
        else
        {
            $batchInfo = @{}
            $requests = @()
            #{"requests":[{"id":"<FilterID>","method":"GET","url":"deviceManagement/assignmentFilters/<FilterID>?$select=displayName"}]}
            foreach($filterId in $filterIds)
            {
                $requests += [PSCustomObject]@{
                    id = $filterId
                    method = "GET"
                    "url" = "deviceManagement/assignmentFilters/$($filterId)?`$select=displayName"
                }
            }
            $batchInfo = @{"requests"=$requests}
            $jsonBody = $batchInfo | ConvertTo-Json

            $filtersInfo = (Invoke-GraphRequest -Url "/`$batch" -Content $jsonBody -Method "Post").responses.body
        }
    }    
    
    @($groupIds, $groupInfo, $filterIds, $filtersInfo)
}

function Get-TableObjects
{
    param($objectTypeId)

    if(-not $objectTypeId -or $script:ObjectTypeFullTable -isnot [HashTable]) { return }

    if($script:ObjectTypeFullTable.ContainsKey($objectTypeId))
    {
        return $script:ObjectTypeFullTable[$objectTypeId]
    }
}

function Set-TableObjects
{
    param($objectInfo)

    if(-not $objectInfo.ObjectType -or $script:ObjectTypeFullTable -isnot [HashTable]) { return }

    if($script:ObjectTypeFullTable.ContainsKey($objectInfo.ObjectType.Id) -eq $false)
    {
        $script:ObjectTypeFullTable.Add($objectInfo.ObjectType.Id, $objectInfo)
    }
}

function Get-PolicyTypeName
{
    param($type, $default = $null)

    $categoryObj =  Get-TranslationFiles $type

    if($null -eq $categoryObj) { return $default }

    $lngStr = Get-LanguageString "PolicyType.$($categoryObj.PolicyTypeLanguageId)"

    if($lngStr) { return $lngStr }

    return $defult
}