Private/Helpers.ps1

function Get-PodeWebTemplatePath {
    $path = Split-Path -Parent -Path ((Get-Module -Name 'Pode.Web' | Sort-Object -Property Version -Descending | Select-Object -First 1).Path)
    return (Join-PodeWebPath $path 'Templates')
}

function Get-PodeWebAuthData {
    $authData = $WebEvent.Auth
    if (($null -eq $authData) -or ($authData.Count -eq 0)) {
        $authData = $WebEvent.Session.Data.Auth
    }

    return $authData
}

function Get-PodeWebAuthUsername {
    param(
        [Parameter()]
        $User
    )

    # nothing if no user
    if ($null -eq $User) {
        return [string]::Empty
    }

    # check username prop
    $prop = (Get-PodeWebState -Name 'auth-props').Username
    if (![string]::IsNullOrEmpty($prop) -and ![string]::IsNullOrEmpty($User.$prop)) {
        return $User.$prop
    }

    # name
    if (![string]::IsNullOrEmpty($User.Name)) {
        return $User.Name
    }

    # full name
    if (![string]::IsNullOrEmpty($User.FullName)) {
        return $User.FullName
    }

    # username
    if (![string]::IsNullOrEmpty($User.Username)) {
        return $User.Username
    }

    # email - split on @ though
    if (![string]::IsNullOrEmpty($User.Email)) {
        return ($User.Email -split '@')[0]
    }

    # nothing
    return [string]::Empty
}

function Get-PodeWebAuthGroups {
    param(
        [Parameter()]
        $User
    )

    # nothing if no auth data
    if ($null -eq $User) {
        return @()
    }

    # check group prop
    $prop = (Get-PodeWebState -Name 'auth-props').Group
    if (![string]::IsNullOrEmpty($prop) -and !(Test-PodeWebArrayEmpty -Array $User.$prop)) {
        return @($User.$prop)
    }

    # groups
    if (!(Test-PodeWebArrayEmpty -Array $User.Groups)) {
        return @($User.Groups)
    }

    # roles
    if (!(Test-PodeWebArrayEmpty -Array $User.Roles)) {
        return @($User.Roles)
    }

    # scopes
    if (!(Test-PodeWebArrayEmpty -Array $User.Scopes)) {
        return @($User.Scopes)
    }

    # nothing
    return @()
}

function Get-PodeWebAuthAvatarUrl {
    param(
        [Parameter()]
        $User
    )

    # nothing if no auth data
    if ($null -eq $User) {
        return [string]::Empty
    }

    # nothing if no property set
    $prop = (Get-PodeWebState -Name 'auth-props').Avatar
    if (![string]::IsNullOrEmpty($prop) -and ![string]::IsNullOrEmpty($User.$prop)) {
        return (Add-PodeWebAppPath -Url $User.$prop)
    }

    # avatar url
    if (![string]::IsNullOrEmpty($User.AvatarUrl)) {
        return (Add-PodeWebAppPath -Url $User.AvatarUrl)
    }

    return [string]::Empty
}

function Get-PodeWebAuthTheme {
    param(
        [Parameter()]
        $User
    )

    # nothing if no auth data
    if ($null -eq $User) {
        return $null
    }

    # nothing if no property set
    $prop = (Get-PodeWebState -Name 'auth-props').Theme
    if (![string]::IsNullOrEmpty($prop) -and ![string]::IsNullOrEmpty($User.$prop)) {
        return $User.$prop
    }

    # theme
    if (![string]::IsNullOrEmpty($User.Theme)) {
        return $User.Theme
    }

    return [string]::Empty
}

function Get-PodeWebInbuiltThemes {
    return @('Auto', 'Light', 'Dark', 'Midnight', 'Sepia', 'Forest', 'Terminal', 'Custom')
}

function Test-PodeWebThemeCustom {
    param(
        [Parameter()]
        [string]
        $Name
    )

    $customThemes = Get-PodeWebState -Name 'custom-themes'
    return ($customThemes.Themes.Keys -icontains $Name)
}

function Test-PodeWebThemeInbuilt {
    param(
        [Parameter()]
        [string]
        $Name
    )

    $inbuildThemes = Get-PodeWebInbuiltThemes
    return ($Name -iin $inbuildThemes)
}

function Get-PodeWebCustomThemeRoutePath {
    return '/pode.web-dynamic/themes/custom' #?name=<name>
}

function Add-PodeWebCustomThemeRoute {
    $path = Get-PodeWebCustomThemeRoutePath
    if (Test-PodeWebRoute -Path $path -Method Get -NoThrow) {
        return
    }

    Add-PodeRoute -Method Get -Path $path -ScriptBlock {
        $name = $WebEvent.Query.Name
        $theme = (Get-PodeWebState -Name 'custom-themes').Themes[$name]

        # theme not found
        if ($null -eq $theme) {
            Set-PodeResponseStatus -Code 404
            return
        }

        # theme invalid
        if ($theme.IsStatic) {
            Set-PodeResponseStatus -Code 400
            return
        }

        # build theme css variables
        $config = $theme.Config
        $builder = [System.Text.StringBuilder]::new()

        if ($config.ColourScheme) {
            $null = $builder.Append("--podeweb-colour-scheme: $($config.ColourScheme);")
        }

        if ($config.FontFamily) {
            $null = $builder.Append("--podeweb-font-family: '$($config.FontFamily -join "','")';")
        }

        if ($config.CodeEditorTheme) {
            $null = $builder.Append("--podeweb-code-editor-theme: $($config.CodeEditorTheme);")
        }

        if ($config.CodeTheme) {
            $null = $builder.Append("--podeweb-code-theme: $($config.CodeTheme);")
        }

        if ($config.BackgroundColourConfig.Count -gt 0) {
            foreach ($bg in $config.BackgroundColourConfig.GetEnumerator()) {
                if ($bg.Value) {
                    $null = $builder.Append("--podeweb-$($bg.Name)-background-color: $($bg.Value);")
                }
            }
        }

        if ($config.BorderColourConfig.Count -gt 0) {
            foreach ($border in $config.BorderColourConfig.GetEnumerator()) {
                if ($border.Value) {
                    $null = $builder.Append("--podeweb-$($border.Name)-border-color: $($border.Value);")
                }
            }
        }

        if ($config.TextColourConfig.Count -gt 0) {
            foreach ($text in $config.TextColourConfig.GetEnumerator()) {
                if ($text.Value) {
                    $null = $builder.Append("--podeweb-$($text.Name)-text-color: $($text.Value);")
                }
            }
        }

        if ($config.NavColourConfig.Count -gt 0) {
            foreach ($nav in $config.NavColourConfig.GetEnumerator()) {
                if ($nav.Value) {
                    $null = $builder.Append("--podeweb-nav-$($nav.Name)-color: $($nav.Value);")
                }
            }
        }

        if ($config.ToastColourConfig.Count -gt 0) {
            foreach ($toast in $config.ToastColourConfig.GetEnumerator()) {
                if ($toast.Value) {
                    $null = $builder.Append("--podeweb-toast-$($toast.Name)-color: $($toast.Value);")
                }
            }
        }

        if ($config.CalendarIconColourConfig.Count -gt 0) {
            foreach ($cal in $config.CalendarIconColourConfig.GetEnumerator()) {
                if ($cal.Value) {
                    $null = $builder.Append("--podeweb-input-calendar-$($cal.Name)-color: $($cal.Value);")
                }
            }
        }

        if ($config.ChartColourConfig.Count -gt 0) {
            foreach ($chart in $config.ChartColourConfig.GetEnumerator()) {
                if ($chart.Value) {
                    $value = $chart.Value
                    if ($chart.Name -ieq 'point') {
                        $value = $value -join ','
                    }

                    $null = $builder.Append("--podeweb-chart-$($chart.Name)-color: $($value);")
                }
            }
        }

        # if builder is empty, just return a 204
        if ($builder.Length -eq 0) {
            Set-PodeResponseStatus -Code 204
            return
        }

        # pre/append ":root { ... }" to builder
        $null = $builder.Insert(0, ':root {').Append('}')

        # convert builder to string, and return
        Write-PodeTextResponse -Value $builder.ToString().ToLowerInvariant() -ContentType 'text/css' -Cache
    }
}

function Test-PodeWebColour {
    param(
        [Parameter()]
        [string[]]
        $Colour,

        [switch]
        $HexOnly,

        [switch]
        $AllowEmpty
    )

    foreach ($c in $Colour) {
        if ($AllowEmpty -and [string]::IsNullOrWhiteSpace($c)) {
            continue
        }

        if ($HexOnly) {
            if ($c -inotmatch '^#([a-f0-9]{6}|[a-f0-9]{3})$') {
                throw "Colour '$($c)' is not a valid hex colour"
            }
        }
        else {
            if ($c -notmatch '^(#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})|[a-z]+|rgb(a)?\(\d+,\s*\d+,\s*\d+(,\s*[\d\.]+)?\))$') {
                throw "Colour '$($c)' is not a valid named, hex, or rgb colour"
            }
        }
    }

    return $Colour
}

function Test-PodeWebArrayEmpty {
    param(
        [Parameter()]
        $Array
    )

    return (($null -eq $Array) -or (@($Array).Length -eq 0))
}

function Test-PodeWebPageAccess {
    param(
        [Parameter()]
        $PageAccess,

        [Parameter()]
        $Auth
    )

    $hasGroups = (!(Test-PodeWebArrayEmpty -Array $PageAccess.Groups))
    $hasUsers = (!(Test-PodeWebArrayEmpty -Array $PageAccess.Users))

    # if page has no access restriction, just return
    if (!$hasGroups -and !$hasUsers) {
        return $true
    }

    # if no auth object, just return
    if ($null -eq $Auth) {
        return $false
    }

    # check groups
    if ($hasGroups -and !(Test-PodeWebArrayEmpty -Array $Auth.Groups)) {
        foreach ($group in $PageAccess.Groups) {
            if ($Auth.Groups -icontains $group) {
                return $true
            }
        }
    }

    # check users
    if ($hasUsers -and ![string]::IsNullOrWhiteSpace($Auth.Username)) {
        if ($PageAccess.Users -icontains $Auth.Username) {
            return $true
        }
    }

    return $false
}

function Write-PodeWebViewResponse {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter()]
        [hashtable]
        $Data = @{}
    )

    $Data['AppPath'] = (Get-PodeWebState -Name 'app-path')
    Write-PodeViewResponse -Path "$($Path).pode" -Data $Data -Folder 'pode.web.views' -FlashMessages
}

function Add-PodeWebAppPath {
    param(
        [Parameter()]
        [string]
        $Url
    )

    if (![string]::IsNullOrWhiteSpace($Url) -and $Url.StartsWith('/')) {
        $appPath = Get-PodeWebState -Name 'app-path'
        $Url = "$($appPath)$($Url)"
    }

    return $Url
}

function Set-PodeWebSystemUrlDefaults {
    Set-PodeWebState -Name 'system-urls' -Value @{
        Home     = @{
            Path     = '/'
            Url      = (Add-PodeWebAppPath -Url '/')
            IsCustom = $false
        }
        Register = @{
            Path = '/register'
            Url  = (Add-PodeWebAppPath -Url '/register')
        }
        Login    = @{
            Path = '/login'
            Url  = (Add-PodeWebAppPath -Url '/login')
        }
        Logout   = @{
            Path = '/logout'
            Url  = (Add-PodeWebAppPath -Url '/logout')
        }
    }
}

function Use-PodeWebPartialView {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter()]
        [hashtable]
        $Data = @{}
    )

    Use-PodePartialView -Path "$($Path).pode" -Data $Data -Folder 'pode.web.views'
}

function Set-PodeWebState {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [object]
        $Value
    )

    Set-PodeState -Name "pode.web.$($Name)" -Value $Value -Scope 'pode.web' | Out-Null
}

function Get-PodeWebState {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    return (Get-PodeState -Name "pode.web.$($Name)")
}

function Get-PodeWebCookie {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    return (Get-PodeCookie -Name "pode.web.$($Name)")
}

function Get-PodeWebRandomName {
    param(
        [Parameter()]
        [int]
        $Length = 5
    )

    if ($PSVersionTable.PSVersion.Major -eq 5) {
        return [string]::Concat(@(foreach ($i in 1..$Length) {
                    [char](Get-Random -Minimum 65 -Maximum 90)
                }))
    }
    else {
        $r = [System.Random]::new()
        return [string]::Concat(@(foreach ($i in 1..$Length) {
                    [char]$r.Next(65, 90)
                }))
    }
}

function Protect-PodeWebName {
    param(
        [Parameter()]
        [string]
        $Name
    )

    return ($Name -ireplace '[^a-z0-9_]', '').Trim()
}

function Protect-PodeWebSpecialCharacters {
    param(
        [Parameter()]
        [string]
        $Value
    )

    return ($Value -replace "[\s!`"#\$%&'\(\)*+,\./:;<=>?@\[\\\]^``{\|}~]", '_')
}

function Protect-PodeWebValue {
    param(
        [Parameter()]
        [string]
        $Value,

        [Parameter()]
        [string]
        $Default,

        [switch]
        $Encode
    )

    if ([string]::IsNullOrWhiteSpace($Value)) {
        if ($Encode) {
            return [System.Net.WebUtility]::HtmlEncode($Default)
        }
        else {
            return $Default
        }
    }

    if ($Encode) {
        return [System.Net.WebUtility]::HtmlEncode($Value)
    }
    else {
        return $Value
    }
}

function Protect-PodeWebValues {
    param(
        [Parameter()]
        [string[]]
        $Value,

        [Parameter()]
        [string[]]
        $Default,

        [switch]
        $EqualCount,

        [switch]
        $Encode
    )

    if (($null -eq $Value) -or ($Value.Length -eq 0)) {
        if (($null -eq $Default) -or ($Default.Length -eq 0)) {
            return
        }

        if ($Encode) {
            return @(foreach ($v in $Default) {
                    [System.Net.WebUtility]::HtmlEncode($v)
                })
        }

        return $Default
    }

    if ($EqualCount -and ($Value.Length -ne $Default.Length)) {
        throw 'Expected an equal number of values in both arrays'
    }

    if ($Encode) {
        return @(foreach ($v in $Value) {
                [System.Net.WebUtility]::HtmlEncode($v)
            })
    }
    else {
        return $Value
    }
}

function Test-PodeWebRoute {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter()]
        [ValidateSet('Get', 'Post', 'Put', 'Delete', 'Patch', 'Head', 'Options')]
        [string]
        $Method = 'Post',

        [switch]
        $NoThrow
    )

    $route = (Get-PodeRoute -Method $Method -Path $Path)

    if (!$NoThrow -and [string]::IsNullOrWhiteSpace($PageData.Name) -and [string]::IsNullOrWhiteSpace($ElementData.Name) -and ($null -ne $route)) {
        throw "An element with ID '$(Split-Path -Path $Path -Leaf)' already exists"
    }

    return ($null -ne $route)
}

function Register-PodeWebPage {
    param(
        [Parameter(Mandatory)]
        [hashtable]
        $Metadata
    )

    # check home page
    if ($Metadata.IsHomePage) {
        $sysUrls = Get-PodeWebState -Name 'system-urls'
        if ($sysUrls.Home.IsCustom) {
            throw "A home page has already been defined at '$($sysUrls.Home.Path)'"
        }

        # update auth success url to home page
        if (![string]::IsNullOrEmpty($Metadata.Authentication)) {
            $auth = Get-PodeAuth -Name $Metadata.Authentication
            if ([string]::IsNullOrWhiteSpace($auth.Success.Url) -or ($auth.Success.Url -ieq $sysUrls.Home.Url)) {
                $auth.Success.Url = $Metadata.Url
            }
        }

        $sysUrls.Home = @{
            Path     = $Metadata.Path
            Url      = $Metadata.Url
            IsCustom = $true
        }
    }

    # register page
    $pages = Get-PodeWebState -Name 'pages'
    $pages[$Metadata.ID] = $Metadata

    # register page to group
    $group = (Get-PodeWebState -Name 'groups')[$Metadata.Group]

    if (!$group.Pages.ContainsKey($Metadata.Index)) {
        $group.Pages.Add($Metadata.Index, @())
    }

    $group.Pages[$Metadata.Index] += $Metadata.ID
}

function Get-PodeWebPageId {
    param(
        [Parameter()]
        [string]
        $Id,

        [Parameter()]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Group,

        [switch]
        $System
    )

    if (![string]::IsNullOrWhiteSpace($Id)) {
        return $Id
    }

    # prep id
    $_id = [string]::Empty

    # internal?
    if ($System) {
        $_id += 'system_'
    }

    # add group
    if (![string]::IsNullOrWhiteSpace($Group)) {
        $_id += "group_$($Group)_"
    }

    # add page
    $_id += "page_$($Name)"

    # protect id, and return
    $_id = Protect-PodeWebName -Name $_id
    return ($_id -replace '\s+', '_').ToLowerInvariant()
}

function Get-PodeWebElementId {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Tag,

        [Parameter()]
        [string]
        $Id,

        [Parameter()]
        [string]
        $Name
    )

    if (![string]::IsNullOrWhiteSpace($Id)) {
        return $Id
    }

    # prepend the parent element's ID
    $_id = [string]::Empty
    if (![string]::IsNullOrWhiteSpace($ElementData.ID)) {
        $_id = "$($ElementData.ID)_"
    }
    elseif (![string]::IsNullOrWhiteSpace($ElementData.Name)) {
        $_id = "$($ElementData.Name)_"
    }

    # start with element tag
    $_id += "$($Tag)"

    # add page name and group if we have one
    if (![string]::IsNullOrWhiteSpace($PageData.ID)) {
        $_id += "_$($PageData.ID)"
    }

    # add name if we have one, or a random name
    if (![string]::IsNullOrWhiteSpace($Name)) {
        $_id += "_$($Name)"
    }
    else {
        $_id += "_$(Get-PodeWebRandomName)"
    }

    $_id = Protect-PodeWebName -Name $_id
    return ($_id -replace '\s+', '_').ToLowerInvariant()
}

function Test-PodeWebContent {
    param(
        [Parameter()]
        [hashtable[]]
        $Content,

        [Parameter()]
        [ValidateSet('Element', 'Navigation', 'Page', 'Group')]
        [string[]]
        $ComponentType,

        [Parameter()]
        [string[]]
        $ObjectType
    )

    # if no content, then it's true
    if (Test-PodeWebArrayEmpty -Array $Content) {
        return $true
    }

    # ensure the content ComponentTypes are correct
    if (!(Test-PodeWebArrayEmpty -Array $ComponentType)) {
        foreach ($item in $Content) {
            if (($item.ComponentType -inotin $ComponentType) -and ($item.Reference.ComponentType -inotin $ComponentType)) {
                return $false
            }
        }
    }

    # ensure the content elements are correct
    if (!(Test-PodeWebArrayEmpty -Array $ObjectType)) {
        foreach ($item in $Content) {
            if (($item.ObjectType -inotin $ObjectType) -and ($item.Reference.ObjectType -inotin $ObjectType)) {
                return $false
            }
        }
    }

    return $true
}

function Remove-PodeWebRoute {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Method,

        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter()]
        [string[]]
        $EndpointName
    )

    if (Test-PodeIsEmpty $EndpointName) {
        Remove-PodeRoute -Method $Method -Path $Path
    }
    else {
        foreach ($endpoint in $EndpointName) {
            Remove-PodeRoute -Method $Method -Path $Path -EndpointName $endpoint
        }
    }
}

function Test-PodeWebOutputWrapped {
    param(
        [Parameter()]
        $Output
    )

    if ($null -eq $Output) {
        return $false
    }

    if ($Output -is [array]) {
        $Output = $Output[0]
    }

    return (($Output -is [hashtable]) -and ![string]::IsNullOrWhiteSpace($Output.Operation) -and ![string]::IsNullOrWhiteSpace($Output.ObjectType))
}

function Split-PodeWebDynamicOutput {
    param(
        [Parameter()]
        [object[]]
        $Output
    )

    if (($null -eq $Output) -or ($Output.Length -eq 0)) {
        return $null, $null
    }

    for ($i = 0; $i -lt $Output.Length; $i++) {
        if (!(Test-PodeWebOutputWrapped -Output $Output[$i])) {
            break
        }
    }

    $wrapped = @()
    if ($i -gt 0) {
        $wrapped = $Output[0..($i - 1)]

        if ($i -lt $Output.Length) {
            $Output = $Output[$i..($Output.Length - 1)]
        }
        else {
            $Output = @()
        }
    }

    return $wrapped, $Output
}

function Join-PodeWebDynamicOutput {
    param(
        [Parameter()]
        [object[]]
        $Wrapped,

        [Parameter()]
        [object[]]
        $Output
    )

    if (($null -eq $Wrapped) -or ($Wrapped.Length -eq 0)) {
        return $Output
    }

    if (($null -eq $Output) -or ($Output.Length -eq 0)) {
        return $Wrapped
    }

    return $Wrapped + $Output
}

function Get-PodeWebFirstPublicPage {
    $pages = Get-PodeWebState -Name 'pages'
    if (($null -eq $pages) -or ($pages.Count -eq 0)) {
        return $null
    }

    foreach ($page in ($pages.Values | Sort-Object -Property { $_.Group }, { $_.Name })) {
        if ($page.IsSystem) {
            continue
        }

        if ((Test-PodeWebArrayEmpty -Array $page.Access.Groups) -and (Test-PodeWebArrayEmpty -Array $page.Access.Users)) {
            return $page
        }
    }

    return $null
}

function Get-PodeWebPagePath {
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Group,

        [Parameter()]
        [string]
        $Path = [string]::Empty,

        [switch]
        $NoAppPath
    )

    # inbuilt page route path if custom not supplied
    if ([string]::IsNullOrEmpty($Path)) {
        $Name = Protect-PodeWebSpecialCharacters -Value $Name
        $Group = Protect-PodeWebSpecialCharacters -Value $Group

        if (![string]::IsNullOrEmpty($Group)) {
            $Path += "/groups/$($Group)"
        }

        $Path += "/pages/$($Name)"
    }

    # check forward slash
    if (!$Path.StartsWith('/')) {
        $Path = "/$($Path)"
    }

    # add app path from IIS
    if (!$NoAppPath) {
        $Path = (Add-PodeWebAppPath -Url $Path)
    }

    return $Path
}

function ConvertTo-PodeWebEvents {
    param(
        [Parameter()]
        [string[]]
        $Events
    )

    $js_events = [string]::Empty

    if (($null -eq $Events) -or ($Events.Length -eq 0)) {
        return $js_events
    }

    foreach ($evt in $Events) {
        $js_events += " on$($evt)=`"invokeEvent('$($evt)', this);`""
    }

    return $js_events
}

function Protect-PodeWebRange {
    param(
        [Parameter()]
        [string]
        $Value,

        [Parameter(Mandatory = $true)]
        [int]
        $Min,

        [Parameter(Mandatory = $true)]
        [int]
        $Max
    )

    # null for no value
    if ([string]::IsNullOrWhiteSpace($Value)) {
        return $null
    }

    $pattern = Get-PodeWebNumberRegex

    # if it's a percentage, calculate value
    if ($Value.EndsWith('%')) {
        $_val = [double]$Value.TrimEnd('%')
        $Value = $Max * $_val * 0.01
    }

    # if value is number, check range
    if ($Value -match $pattern) {
        $_val = [int]$Value

        if ($_val -lt $Min) {
            return $Min
        }

        if ($_val -gt $Max) {
            return $Max
        }

        return $_val
    }

    # invalid value
    throw "Invalid value supplied for range: $($Value). Expected a value between $($Min)-$($Max), or a percentage."
}

function ConvertTo-PodeWebSize {
    param(
        [Parameter()]
        [string]
        $Value,

        [Parameter()]
        [string]
        $Default = 0,

        [Parameter(Mandatory = $true)]
        [ValidateSet('px', '%', 'em')]
        [string]
        $Type,

        [switch]
        $AllowNull
    )

    if ($AllowNull -and [string]::IsNullOrEmpty($Value)) {
        return $null
    }

    $pattern = Get-PodeWebNumberRegex
    $defIsNumber = ($Default -match $pattern)

    if ([string]::IsNullOrWhiteSpace($Value)) {
        if ($defIsNumber) {
            return "$($Default)$($Type)"
        }
        else {
            return $Default
        }
    }

    if ($Value -match $pattern) {
        $_val = [double]$Value
        if ($_val -le 0) {
            if ($defIsNumber) {
                $Value = $Default
            }
            else {
                return $Default
            }
        }
        elseif (($Type -eq '%') -and ($_val -gt 100)) {
            $Value = 100
        }

        return "$($Value)$($Type)"
    }

    return $Value
}

function Get-PodeWebNumberRegex {
    return '^\-?\d+(\.\d+){0,1}$'
}

function Set-PodeWebSecurity {
    param(
        [Parameter()]
        [ValidateSet('None', 'Default', 'Simple', 'Strict')]
        [string]
        $Security,

        [switch]
        $UseHsts
    )

    if ($Security -ieq 'none') {
        Remove-PodeSecurity
        return
    }

    switch ($Security.ToLowerInvariant()) {
        'default' {
            Set-PodeSecurity -Type Simple -UseHsts:$UseHsts
            Remove-PodeSecurityCrossOrigin

            Add-PodeSecurityContentSecurityPolicy `
                -Default 'http', 'https' `
                -Style 'http', 'https' `
                -Scripts 'http', 'https' `
                -Image 'http', 'https'
        }

        'simple' {
            Set-PodeSecurity -Type Simple -UseHsts:$UseHsts
        }

        'strict' {
            Set-PodeSecurity -Type Strict -UseHsts:$UseHsts
        }
    }

    Add-PodeSecurityContentSecurityPolicy `
        -Style 'self', 'unsafe-inline' `
        -Scripts 'self', 'unsafe-inline' `
        -Image 'self', 'data'
}

function Test-PodeWebParameter {
    param(
        [Parameter(Mandatory = $true)]
        $Parameters,

        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        $Value
    )

    if ($Parameters.ContainsKey($Name)) {
        return $Value
    }

    return $null
}

function Protect-PodeWebIconType {
    param(
        [Parameter()]
        [object]
        $Icon,

        [Parameter(Mandatory = $true)]
        [string]
        $Element
    )

    # just null or string
    if (($null -eq $Icon) -or ($Icon -is [string])) {
        return $Icon
    }

    # if hashtable, check object type
    if (($Icon -is [hashtable]) -and ($Icon.ObjectType -ieq 'icon')) {
        return $Icon
    }

    # error
    throw "Icon for '$($Element)' is not a string or hashtable from New-PodeWebIcon"
}

function Protect-PodeWebIconPreset {
    param(
        [Parameter(Mandatory = $true)]
        [hashtable]
        $Icon,

        [Parameter()]
        [hashtable]
        $Preset
    )

    if (($null -eq $Preset) -or ($Preset.Length -eq 0)) {
        return $null
    }

    if ([string]::IsNullOrWhiteSpace($Preset.Name)) {
        $Preset.Name = $Icon.Name
    }

    if ([string]::IsNullOrWhiteSpace($Preset.Colour)) {
        $Preset.Colour = $Icon.Colour
    }

    if ([string]::IsNullOrWhiteSpace($Preset.Title)) {
        $Preset.Title = $Icon.Title
    }

    if ([string]::IsNullOrWhiteSpace($Preset.Flip)) {
        $Preset.Flip = $Icon.Flip
    }

    if ($Preset.Rotate -le -1) {
        $Preset.Rotate = $Icon.Rotate
    }

    if ($Preset.Size -le -1) {
        $Preset.Size = $Icon.Size
    }

    if ($null -eq $Preset.Spin) {
        $Preset.Spin = $Icon.Spin
    }

    return $Preset
}

function Invoke-PodeWebScriptBlock {
    param(
        [Parameter(Mandatory = $true)]
        [hashtable]
        $Logic,

        [Parameter()]
        $Arguments = $null
    )

    $result = Invoke-PodeScriptBlock -ScriptBlock $Logic.ScriptBlock -Arguments $Arguments -UsingVariables $Logic.UsingVariables -Splat -Return
    if ($null -eq $result) {
        $result = @()
    }

    return $result
}

function Set-PodeWebMetadata {
    $WebEvent.Metadata.SenderId = Get-PodeHeader -Name 'X-PODE-WEB-SENDER-ID'
}

function Test-PodeWebResponseType {
    param(
        [Parameter()]
        [ValidateSet('Http', 'Sse')]
        [string]
        $Type
    )

    return ((Get-PodeWebState -Name 'resp-type') -ieq $Type)
}

function Get-PodeWebResponseType {
    return (Get-PodeWebState -Name 'resp-type')
}