Classes/PSColorStyle.ps1

<#
.SYNOPSIS
    PSColorStyle class for managing reusable color and style configurations
 
.DESCRIPTION
    A PSColorStyle holds the colors, styles and layout settings of a Write-ColorEX call, so
    they can be reused with -StyleProfile.
 
    - [PSColorStyle]::Default is the style -Default applies
    - [PSColorStyle]::Profiles holds named styles
    - Built-in profiles: Default, Error, Warning, Info, Success, Critical, Debug
    - ToWriteColorParams() returns the Write-ColorEX parameters a style sets, read from its
      properties on each call, so a change to a style applies to its next use
    - Clone() copies a style
 
    USAGE PATTERN:
    - Create custom styles with New-ColorStyle function
    - Access built-in profiles: [PSColorStyle]::Profiles['Error']
    - Set default style: $style.SetAsDefault()
    - Register profiles: $style.AddToProfiles()
 
.NOTES
    Author: Mark Newton
    License: MIT
    Requires: PowerShell 5.1 or later
 
    PSColorStyle is one .NET type for the whole process. The first import builds it with
    Reflection.Emit and every later import, in any runspace, takes the same type, so a style made
    in one runspace works in every other. Its properties, its constructors and the Default and
    Profiles it keeps are plain data. Each of its methods runs the functions below in the module
    of the runspace that calls it, so no method depends on the runspace that made the style or
    imported the module, which can be disposed while the others go on.
 
    Importing the module makes [PSColorStyle] usable in the session and creates the built-in
    profiles with InitializeDefaultProfiles().
 
.LINK
    https://github.com/MarkusMcNugen/PSWriteColorEX
 
.LINK
    New-ColorStyle
 
.LINK
    Set-ColorDefault
 
.EXAMPLE
    # Create and register a custom style
    $style = [PSColorStyle]::new("Header", "Cyan", $null)
    $style.Bold = $true
    $style.Underline = $true
    $style.HorizontalCenter = $true
    $style.AddToProfiles()
 
.EXAMPLE
    # Use a built-in profile
    $errorStyle = [PSColorStyle]::Profiles['Error']
    Write-ColorEX -Text "Failed!" -StyleProfile $errorStyle
 
.EXAMPLE
    # Clone and modify a profile
    $customError = [PSColorStyle]::Profiles['Error'].Clone()
    $customError.Name = "MyError"
    $customError.Italic = $true
#>


# PSColorStyle's properties in order, each with its type
$script:ColorStyleProperties = [ordered]@{
    Name               = [string]
    ForegroundColor    = [object]
    BackgroundColor    = [object]
    Gradient           = [object[]]
    Style              = [string[]]
    StartTab           = [int]
    StartSpaces        = [int]
    LinesBefore        = [int]
    LinesAfter         = [int]
    Bold               = [bool]
    Italic             = [bool]
    Underline          = [bool]
    Blink              = [bool]
    Faint              = [bool]
    CrossedOut         = [bool]
    DoubleUnderline    = [bool]
    Overline           = [bool]
    ShowTime           = [bool]
    NoNewLine          = [bool]
    HorizontalCenter   = [bool]
    AutoPad            = [int]
    PadLeft            = [bool]
    PadChar            = [char]
    BackgroundGradient = [object[]]
    GradientSpace      = [string]
    GradientHue        = [string]
    GradientEasing     = [string]
    GradientRepeat     = [string]
    GradientDirection  = [string]
    GradientSteps      = [int]
    Reverse            = [bool]
    UnderlineColor     = [object]
    UnderlineStyle     = [string]
    PadCenter          = [bool]
    Truncate           = [bool]
    Wrap               = [bool]
}

# PSColorStyle's methods: whether each is static and whether hidden, what it returns ('Self' for
# a PSColorStyle, $null for nothing) and its parameters in order
$script:ColorStyleMethods = @(
    [pscustomobject]@{ Name = 'Initialize'; Static = $false; Hidden = $true; Returns = $null; Parameters = [ordered]@{ name = [string]; foreground = [object]; background = [object] } }
    [pscustomobject]@{ Name = 'SetAsDefault'; Static = $false; Hidden = $false; Returns = $null; Parameters = [ordered]@{} }
    [pscustomobject]@{ Name = 'AddToProfiles'; Static = $false; Hidden = $false; Returns = $null; Parameters = [ordered]@{} }
    [pscustomobject]@{ Name = 'GetProfile'; Static = $true; Hidden = $false; Returns = 'Self'; Parameters = [ordered]@{ name = [string] } }
    [pscustomobject]@{ Name = 'InitializeDefaultProfiles'; Static = $true; Hidden = $false; Returns = $null; Parameters = [ordered]@{} }
    [pscustomobject]@{ Name = 'ToWriteColorParams'; Static = $false; Hidden = $false; Returns = [hashtable]; Parameters = [ordered]@{} }
    [pscustomobject]@{ Name = 'InvalidateCache'; Static = $false; Hidden = $true; Returns = $null; Parameters = [ordered]@{} }
    [pscustomobject]@{ Name = 'Clone'; Static = $false; Hidden = $false; Returns = 'Self'; Parameters = [ordered]@{} }
    [pscustomobject]@{ Name = 'IsGradient'; Static = $true; Hidden = $true; Returns = [bool]; Parameters = [ordered]@{ Value = [object[]] } }
    [pscustomobject]@{ Name = 'CopyValue'; Static = $true; Hidden = $true; Returns = [object]; Parameters = [ordered]@{ Value = [object] } }
)

# The layout of what Initialize-ColorStyleType emits beyond the tables above; part of the type's
# assembly name with them, so a module that emits a different type never takes this one
$script:ColorStyleLayout = 1

# The name a runspace's script block for PSColorStyle's methods is kept under in the process,
# followed by the runspace's id
$script:ColorStyleMemberKey = 'PSWriteColorEX.PSColorStyle.'

# The script block PSColorStyle's methods run in this runspace
$script:ColorStyleMember = {
    param([string] $Member, [object] $Style, [object[]] $Arguments)
    Invoke-ColorStyleMember -Member $Member -Style $Style -Arguments $Arguments
}

function Get-ColorStyleShape {
    <#
    .SYNOPSIS
    A hash of PSColorStyle's members and layout, which names the assembly the type is built in.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param()

    $members = @("layout $script:ColorStyleLayout")
    foreach ($name in $script:ColorStyleProperties.Keys) {
        $members += "$name $($script:ColorStyleProperties[$name].FullName)"
    }
    foreach ($method in $script:ColorStyleMethods) {
        $parameters = foreach ($name in $method.Parameters.Keys) { "$name $($method.Parameters[$name].FullName)" }
        $members += "$($method.Name) $($method.Static) $($method.Hidden) $($method.Returns) $($parameters -join ',')"
    }
    $sha = [System.Security.Cryptography.SHA256]::Create()
    try {
        $hash = $sha.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($members -join "`n"))
    } finally {
        $sha.Dispose()
    }
    return -join ($hash[0..7] | ForEach-Object { $_.ToString('x2') })
}

function Get-ColorStyleType {
    <#
    .SYNOPSIS
    Returns this process's PSColorStyle type, building it the first time.
 
    .DESCRIPTION
    The type lives in an assembly named after the hash of its members, so every import of a
    module with the same members, in any runspace, takes the same type: the one [PSColorStyle]
    names already, or else the one an earlier import built, or else one built now. The first type
    of these members that [PSColorStyle] names stays named, so imports that build at the same
    moment all take the type that was named first; a type with other members, from another module
    or version, is replaced, as each import names its own.
    #>

    [CmdletBinding()]
    [OutputType([type])]
    param()

    $assemblyName = "PSWriteColorEX.PSColorStyle.$(Get-ColorStyleShape)"
    $accelerators = [psobject].Assembly.GetType('System.Management.Automation.TypeAccelerators')
    $named = Get-ColorStyleAccelerator -AssemblyName $assemblyName
    if ($null -ne $named) {
        return $named
    }
    $built = $null
    foreach ($assembly in [System.AppDomain]::CurrentDomain.GetAssemblies()) {
        if ($null -eq $built -and $assembly.IsDynamic -and $assembly.GetName().Name -eq $assemblyName) {
            $built = $assembly.GetType('PSColorStyle')
        }
    }
    if ($null -eq $built) {
        $built = Initialize-ColorStyleType -AssemblyName $assemblyName
    }
    $named = Get-ColorStyleAccelerator -AssemblyName $assemblyName
    if ($null -ne $named) {
        return $named
    }
    $accelerators::Add('PSColorStyle', $built)
    return $built
}

function Get-ColorStyleAccelerator {
    <#
    .SYNOPSIS
    The type [PSColorStyle] names when it is one from the assembly named, or $null.
    #>

    [CmdletBinding()]
    [OutputType([type])]
    param([Parameter(Mandatory)][string] $AssemblyName)

    $named = ([psobject].Assembly.GetType('System.Management.Automation.TypeAccelerators'))::Get
    if ($named.ContainsKey('PSColorStyle') -and $named['PSColorStyle'].Assembly.GetName().Name -eq $AssemblyName) {
        return $named['PSColorStyle']
    }
    return $null
}

function Add-ColorStyleProperty {
    <#
    .SYNOPSIS
    Adds a property to the type being built: a field, and the get and set methods that read and
    write it. Returns the field.
    #>

    [CmdletBinding()]
    [OutputType([System.Reflection.Emit.FieldBuilder])]
    param(
        [Parameter(Mandatory)][System.Reflection.Emit.TypeBuilder] $Type,
        [Parameter(Mandatory)][string] $Name,
        [Parameter(Mandatory)][type] $PropertyType,
        [switch] $Static
    )

    $op = [System.Reflection.Emit.OpCodes]
    $fieldAttributes = [System.Reflection.FieldAttributes]::Private
    $methodAttributes = [System.Reflection.MethodAttributes]'Public, HideBySig, SpecialName'
    $callingConvention = [System.Reflection.CallingConventions]::HasThis
    if ($Static) {
        $fieldAttributes = $fieldAttributes -bor [System.Reflection.FieldAttributes]::Static
        $methodAttributes = $methodAttributes -bor [System.Reflection.MethodAttributes]::Static
        $callingConvention = [System.Reflection.CallingConventions]::Standard
    }
    $field = $Type.DefineField("<$Name>k__BackingField", $PropertyType, $fieldAttributes)
    $property = $Type.DefineProperty($Name, [System.Reflection.PropertyAttributes]::None, $callingConvention, $PropertyType, [type[]]@())

    $getter = $Type.DefineMethod("get_$Name", $methodAttributes, $callingConvention, $PropertyType, [type[]]@())
    $il = $getter.GetILGenerator()
    if ($Static) {
        $il.Emit($op::Ldsfld, [System.Reflection.FieldInfo]$field)
    } else {
        $il.Emit($op::Ldarg_0)
        $il.Emit($op::Ldfld, [System.Reflection.FieldInfo]$field)
    }
    $il.Emit($op::Ret)

    $setter = $Type.DefineMethod("set_$Name", $methodAttributes, $callingConvention, [void], [type[]]@($PropertyType))
    $null = $setter.DefineParameter(1, [System.Reflection.ParameterAttributes]::None, 'value')
    $il = $setter.GetILGenerator()
    $il.Emit($op::Ldarg_0)
    if ($Static) {
        $il.Emit($op::Stsfld, [System.Reflection.FieldInfo]$field)
    } else {
        $il.Emit($op::Ldarg_1)
        $il.Emit($op::Stfld, [System.Reflection.FieldInfo]$field)
    }
    $il.Emit($op::Ret)

    $property.SetGetMethod($getter)
    $property.SetSetMethod($setter)
    return $field
}

function Initialize-ColorStyleType {
    <#
    .SYNOPSIS
    Builds the PSColorStyle type with Reflection.Emit, in a new assembly of the name given.
 
    .DESCRIPTION
    The properties and constructors are plain data. Each method puts its arguments in an array
    and hands them, with its own name and the style, to InvokeMember, which runs the script block
    the calling runspace's import registered under that runspace's id; the method converts what
    comes back to the type it returns. Enter and Leave count the imports that use the type.
    #>

    [CmdletBinding()]
    [OutputType([type])]
    param([Parameter(Mandatory)][string] $AssemblyName)

    $op = [System.Reflection.Emit.OpCodes]
    $assembly = [System.Reflection.Emit.AssemblyBuilder]::DefineDynamicAssembly(
        [System.Reflection.AssemblyName]::new($AssemblyName), [System.Reflection.Emit.AssemblyBuilderAccess]::Run)
    $type = $assembly.DefineDynamicModule($AssemblyName).DefineType(
        'PSColorStyle', [System.Reflection.TypeAttributes]'Public, Class, BeforeFieldInit', [object])

    $fields = @{}
    foreach ($name in $script:ColorStyleProperties.Keys) {
        $fields[$name] = Add-ColorStyleProperty -Type $type -Name $name -PropertyType $script:ColorStyleProperties[$name]
    }
    $null = Add-ColorStyleProperty -Type $type -Name 'Default' -PropertyType $type -Static
    $profiles = Add-ColorStyleProperty -Type $type -Name 'Profiles' -PropertyType ([hashtable]) -Static

    # Profiles starts as an empty table that compares names without regard to case, as @{} does
    $il = $type.DefineTypeInitializer().GetILGenerator()
    $il.Emit($op::Call, [System.StringComparer].GetProperty('CurrentCultureIgnoreCase').GetGetMethod())
    $il.Emit($op::Newobj, [hashtable].GetConstructor([type[]]@([System.Collections.IEqualityComparer])))
    $il.Emit($op::Stsfld, [System.Reflection.FieldInfo]$profiles)
    $il.Emit($op::Ret)

    # Each constructor sets what Initialize sets: the name and colors given, the name Custom and
    # the color Gray where none is given, an empty list of styles, a space to pad with and empty
    # gradient and underline settings
    $constructors = @(
        [ordered]@{}
        [ordered]@{ name = [string] }
        [ordered]@{ name = [string]; foreground = [object]; background = [object] }
    )
    foreach ($parameters in $constructors) {
        $constructor = $type.DefineConstructor([System.Reflection.MethodAttributes]'Public, HideBySig, SpecialName, RTSpecialName',
            [System.Reflection.CallingConventions]::Standard, [type[]]@($parameters.Values))
        $position = 1
        foreach ($parameter in $parameters.Keys) {
            $null = $constructor.DefineParameter($position, [System.Reflection.ParameterAttributes]::None, $parameter)
            $position++
        }
        $il = $constructor.GetILGenerator()
        $il.Emit($op::Ldarg_0)
        $il.Emit($op::Call, [object].GetConstructor([type[]]@()))
        $il.Emit($op::Ldarg_0)
        if ($parameters.Count -ge 1) { $il.Emit($op::Ldarg_1) } else { $il.Emit($op::Ldstr, 'Custom') }
        $il.Emit($op::Stfld, [System.Reflection.FieldInfo]$fields['Name'])
        $il.Emit($op::Ldarg_0)
        if ($parameters.Count -ge 3) { $il.Emit($op::Ldarg_2) } else { $il.Emit($op::Ldstr, 'Gray') }
        $il.Emit($op::Stfld, [System.Reflection.FieldInfo]$fields['ForegroundColor'])
        if ($parameters.Count -ge 3) {
            $il.Emit($op::Ldarg_0)
            $il.Emit($op::Ldarg_3)
            $il.Emit($op::Stfld, [System.Reflection.FieldInfo]$fields['BackgroundColor'])
        }
        $il.Emit($op::Ldarg_0)
        $il.Emit($op::Ldc_I4_0)
        $il.Emit($op::Newarr, [string])
        $il.Emit($op::Stfld, [System.Reflection.FieldInfo]$fields['Style'])
        $il.Emit($op::Ldarg_0)
        $il.Emit($op::Ldc_I4, [int][char]' ')
        $il.Emit($op::Stfld, [System.Reflection.FieldInfo]$fields['PadChar'])
        foreach ($name in 'GradientSpace', 'GradientHue', 'GradientEasing', 'GradientRepeat', 'GradientDirection', 'UnderlineStyle') {
            $il.Emit($op::Ldarg_0)
            $il.Emit($op::Ldstr, '')
            $il.Emit($op::Stfld, [System.Reflection.FieldInfo]$fields[$name])
        }
        $il.Emit($op::Ret)
    }

    # InvokeMember(name, style, arguments): the calling runspace's script block, found under its
    # id, run with the three; a runspace that has not imported the module has none
    $invoke = $type.DefineMethod('InvokeMember', [System.Reflection.MethodAttributes]'Private, Static, HideBySig',
        [System.Reflection.CallingConventions]::Standard, [object], [type[]]@([string], [object], [object[]]))
    $il = $invoke.GetILGenerator()
    $runspace = $il.DeclareLocal([runspace])
    $id = $il.DeclareLocal([guid])
    $handler = $il.DeclareLocal([scriptblock])
    $missing = $il.DefineLabel()
    $missingPop = $il.DefineLabel()
    $il.Emit($op::Call, [runspace].GetProperty('DefaultRunspace').GetGetMethod())
    $il.Emit($op::Stloc, $runspace)
    $il.Emit($op::Ldloc, $runspace)
    $il.Emit($op::Brfalse, $missing)
    $il.Emit($op::Call, [System.AppDomain].GetProperty('CurrentDomain').GetGetMethod())
    $il.Emit($op::Ldstr, $script:ColorStyleMemberKey)
    $il.Emit($op::Ldloc, $runspace)
    $il.Emit($op::Callvirt, [runspace].GetProperty('InstanceId').GetGetMethod())
    $il.Emit($op::Stloc, $id)
    $il.Emit($op::Ldloca, $id)
    $il.Emit($op::Call, [guid].GetMethod('ToString', [type[]]@()))
    $il.Emit($op::Call, [string].GetMethod('Concat', [type[]]@([string], [string])))
    $il.Emit($op::Callvirt, [System.AppDomain].GetMethod('GetData', [type[]]@([string])))
    $il.Emit($op::Isinst, [System.WeakReference])
    $il.Emit($op::Dup)
    $il.Emit($op::Brfalse, $missingPop)
    $il.Emit($op::Callvirt, [System.WeakReference].GetProperty('Target').GetGetMethod())
    $il.Emit($op::Isinst, [scriptblock])
    $il.Emit($op::Stloc, $handler)
    $il.Emit($op::Ldloc, $handler)
    $il.Emit($op::Brfalse, $missing)
    $il.Emit($op::Ldloc, $handler)
    $il.Emit($op::Ldc_I4_3)
    $il.Emit($op::Newarr, [object])
    for ($i = 0; $i -lt 3; $i++) {
        $il.Emit($op::Dup)
        $il.Emit($op::Ldc_I4, $i)
        $il.Emit($op::Ldarg, [int16]$i)
        $il.Emit($op::Stelem_Ref)
    }
    $il.Emit($op::Callvirt, [scriptblock].GetMethod('InvokeReturnAsIs'))
    $il.Emit($op::Ret)
    $il.MarkLabel($missingPop)
    $il.Emit($op::Pop)
    $il.MarkLabel($missing)
    $il.Emit($op::Ldstr, 'PSWriteColorEX is not imported in this runspace, so the PSColorStyle method has nothing to run it with. Import the module in this runspace first.')
    $il.Emit($op::Newobj, [System.InvalidOperationException].GetConstructor([type[]]@([string])))
    $il.Emit($op::Throw)

    # Enter and Leave: one more and one fewer import using the type, each returning the count
    $imports = $type.DefineField('imports', [int], [System.Reflection.FieldAttributes]'Private, Static')
    foreach ($counter in @(@('Enter', 'Increment'), @('Leave', 'Decrement'))) {
        $count = $type.DefineMethod($counter[0], [System.Reflection.MethodAttributes]'Private, Static, HideBySig',
            [System.Reflection.CallingConventions]::Standard, [int], [type[]]@())
        $il = $count.GetILGenerator()
        $il.Emit($op::Ldsflda, [System.Reflection.FieldInfo]$imports)
        $il.Emit($op::Call, [System.Threading.Interlocked].GetMethod($counter[1], [type[]]@([int].MakeByRefType())))
        $il.Emit($op::Ret)
    }

    # The methods: arguments in an array, InvokeMember, and what it returns converted
    $getTypeFromHandle = [type].GetMethod('GetTypeFromHandle')
    $convertTo = [System.Management.Automation.LanguagePrimitives].GetMethod('ConvertTo', [type[]]@([object], [type]))
    $hidden = [System.Reflection.Emit.CustomAttributeBuilder]::new([System.Management.Automation.HiddenAttribute].GetConstructor([type[]]@()), [object[]]@())
    foreach ($method in $script:ColorStyleMethods) {
        $attributes = [System.Reflection.MethodAttributes]'Public, HideBySig'
        $callingConvention = [System.Reflection.CallingConventions]::HasThis
        $first = 1
        if ($method.Static) {
            $attributes = $attributes -bor [System.Reflection.MethodAttributes]::Static
            $callingConvention = [System.Reflection.CallingConventions]::Standard
            $first = 0
        }
        $returnType = [void]
        if ($method.Returns -is [string]) {
            $returnType = $type
        } elseif ($null -ne $method.Returns) {
            $returnType = $method.Returns
        }
        $parameterTypes = [type[]]@($method.Parameters.Values)
        $builder = $type.DefineMethod($method.Name, $attributes, $callingConvention, $returnType, $parameterTypes)
        $position = 1
        foreach ($parameter in $method.Parameters.Keys) {
            $null = $builder.DefineParameter($position, [System.Reflection.ParameterAttributes]::None, $parameter)
            $position++
        }
        if ($method.Hidden) {
            $builder.SetCustomAttribute($hidden)
        }
        $il = $builder.GetILGenerator()
        $il.Emit($op::Ldstr, $method.Name)
        if ($method.Static) { $il.Emit($op::Ldnull) } else { $il.Emit($op::Ldarg_0) }
        $il.Emit($op::Ldc_I4, $parameterTypes.Count)
        $il.Emit($op::Newarr, [object])
        for ($i = 0; $i -lt $parameterTypes.Count; $i++) {
            $il.Emit($op::Dup)
            $il.Emit($op::Ldc_I4, $i)
            $il.Emit($op::Ldarg, [int16]($i + $first))
            if ($parameterTypes[$i].IsValueType) {
                $il.Emit($op::Box, $parameterTypes[$i])
            }
            $il.Emit($op::Stelem_Ref)
        }
        $il.Emit($op::Call, [System.Reflection.MethodInfo]$invoke)
        if ($returnType -eq [void]) {
            $il.Emit($op::Pop)
        } elseif ($returnType -ne [object]) {
            $il.Emit($op::Ldtoken, $returnType)
            $il.Emit($op::Call, $getTypeFromHandle)
            $il.Emit($op::Call, $convertTo)
            if ($returnType.IsValueType) {
                $il.Emit($op::Unbox_Any, $returnType)
            } else {
                $il.Emit($op::Castclass, $returnType)
            }
        }
        $il.Emit($op::Ret)
    }

    return $type.CreateType()
}

function Register-ColorStyleType {
    <#
    .SYNOPSIS
    Keeps this runspace's script block for PSColorStyle's methods where the type finds it, and
    counts this import. Returns the name the script block is kept under.
 
    .DESCRIPTION
    The script block is kept in the process under this runspace's id, through a weak reference,
    so a disposed runspace's module is not held in memory by it.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param([Parameter(Mandatory)][type] $Type)

    $key = $script:ColorStyleMemberKey + [runspace]::DefaultRunspace.InstanceId.ToString()
    [System.AppDomain]::CurrentDomain.SetData($key, [System.WeakReference]::new($script:ColorStyleMember))
    $null = $Type.GetMethod('Enter', [System.Reflection.BindingFlags]'NonPublic, Static').Invoke($null, $null)
    return $key
}

function Invoke-ColorStyleMember {
    <#
    .SYNOPSIS
    Runs the PSColorStyle method named, for the style and with the arguments given.
    #>

    [CmdletBinding()]
    [OutputType([hashtable], [bool], [object])]
    [OutputType([PSColorStyle])]
    param(
        [Parameter(Mandatory)][string] $Member,
        [AllowNull()][object] $Style,
        [AllowNull()][object[]] $Arguments
    )

    switch ($Member) {
        'Initialize' { Initialize-ColorStyle -Style $Style -Name $Arguments[0] -Foreground $Arguments[1] -Background $Arguments[2] }
        'SetAsDefault' { [PSColorStyle]::Default = $Style }
        'AddToProfiles' { [PSColorStyle]::Profiles[$Style.Name] = $Style }
        'GetProfile' { Get-ColorStyleProfile -Name $Arguments[0] }
        'InitializeDefaultProfiles' { Initialize-ColorDefaultProfile }
        'ToWriteColorParams' { Get-ColorStyleParameter -Style $Style }
        'InvalidateCache' { }
        'Clone' { Copy-ColorStyle -Style $Style }
        'IsGradient' { Test-ColorStyleGradient -Value $Arguments[0] }
        'CopyValue' { Copy-ColorStyleValue -Value $Arguments[0] }
    }
}

function Initialize-ColorStyle {
    <#
    .SYNOPSIS
    Sets every property of a style as its constructors do: the name and colors given, an empty
    list of styles, a space to pad with, and nothing else set.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][object] $Style,
        [AllowNull()][string] $Name,
        [AllowNull()][object] $Foreground,
        [AllowNull()][object] $Background
    )

    $Style.Name = $Name
    $Style.ForegroundColor = $Foreground
    $Style.BackgroundColor = $Background
    $Style.Gradient = $null
    $Style.Style = @()
    $Style.StartTab = 0
    $Style.StartSpaces = 0
    $Style.LinesBefore = 0
    $Style.LinesAfter = 0
    $Style.Bold = $false
    $Style.Italic = $false
    $Style.Underline = $false
    $Style.Blink = $false
    $Style.Faint = $false
    $Style.CrossedOut = $false
    $Style.DoubleUnderline = $false
    $Style.Overline = $false
    $Style.ShowTime = $false
    $Style.NoNewLine = $false
    $Style.HorizontalCenter = $false
    $Style.AutoPad = 0
    $Style.PadLeft = $false
    $Style.PadChar = ' '
    $Style.BackgroundGradient = $null
    $Style.GradientSpace = ''
    $Style.GradientHue = ''
    $Style.GradientEasing = ''
    $Style.GradientRepeat = ''
    $Style.GradientDirection = ''
    $Style.GradientSteps = 0
    $Style.Reverse = $false
    $Style.UnderlineColor = $null
    $Style.UnderlineStyle = ''
    $Style.PadCenter = $false
    $Style.Truncate = $false
    $Style.Wrap = $false
}

function Get-ColorStyleProfile {
    <#
    .SYNOPSIS
    The profile of the name given in [PSColorStyle]::Profiles, or $null.
    #>

    [CmdletBinding()]
    [OutputType([PSColorStyle])]
    param([AllowNull()][string] $Name)

    $styleProfile = [PSColorStyle]::Profiles[$Name]
    if ($styleProfile) {
        return $styleProfile
    }
    return $null
}

function Initialize-ColorDefaultProfile {
    <#
    .SYNOPSIS
    Puts the seven built-in profiles in [PSColorStyle]::Profiles, each one new, and makes the
    Default profile the style -Default applies.
    #>

    [CmdletBinding()]
    param()

    $defaultProfile = [PSColorStyle]::new('Default', 'Gray', $null)
    [PSColorStyle]::Default = $defaultProfile
    [PSColorStyle]::Profiles['Default'] = $defaultProfile

    $errorProfile = [PSColorStyle]::new('Error', 'Red', $null)
    $errorProfile.Bold = $true
    [PSColorStyle]::Profiles['Error'] = $errorProfile

    [PSColorStyle]::Profiles['Warning'] = [PSColorStyle]::new('Warning', 'Yellow', $null)
    [PSColorStyle]::Profiles['Info'] = [PSColorStyle]::new('Info', 'Cyan', $null)
    [PSColorStyle]::Profiles['Success'] = [PSColorStyle]::new('Success', 'Green', $null)

    $criticalProfile = [PSColorStyle]::new('Critical', 'White', 'DarkRed')
    $criticalProfile.Bold = $true
    $criticalProfile.Blink = $true
    [PSColorStyle]::Profiles['Critical'] = $criticalProfile

    $debugProfile = [PSColorStyle]::new('Debug', 'DarkGray', $null)
    $debugProfile.Italic = $true
    [PSColorStyle]::Profiles['Debug'] = $debugProfile
}

function Get-ColorStyleParameter {
    <#
    .SYNOPSIS
    The Write-ColorEX parameters a style sets, read from its properties.
    #>

    [CmdletBinding()]
    [OutputType([hashtable])]
    param([Parameter(Mandatory)][object] $Style)

    $params = @{}

    # A gradient replaces the foreground color
    if ($null -ne $Style.Gradient -and (Test-ColorStyleGradient -Value $Style.Gradient)) {
        $params['Gradient'] = $Style.Gradient
    } elseif ($Style.ForegroundColor) {
        $params['Color'] = $Style.ForegroundColor
    }
    # A background gradient replaces the background color
    if ($null -ne $Style.BackgroundGradient -and (Test-ColorStyleGradient -Value $Style.BackgroundGradient)) {
        $params['BackGroundGradient'] = $Style.BackgroundGradient
    } elseif ($Style.BackgroundColor) {
        $params['BackGroundColor'] = $Style.BackgroundColor
    }
    if ($Style.GradientSpace) { $params['GradientSpace'] = $Style.GradientSpace }
    if ($Style.GradientHue) { $params['GradientHue'] = $Style.GradientHue }
    if ($Style.GradientEasing) { $params['GradientEasing'] = $Style.GradientEasing }
    if ($Style.GradientRepeat) { $params['GradientRepeat'] = $Style.GradientRepeat }
    if ($Style.GradientDirection) { $params['GradientDirection'] = $Style.GradientDirection }
    if ($Style.GradientSteps -gt 0) { $params['GradientSteps'] = $Style.GradientSteps }
    if ($Style.Style.Count -gt 0) { $params['Style'] = $Style.Style }
    if ($Style.StartTab -gt 0) { $params['StartTab'] = $Style.StartTab }
    if ($Style.StartSpaces -gt 0) { $params['StartSpaces'] = $Style.StartSpaces }
    if ($Style.LinesBefore -gt 0) { $params['LinesBefore'] = $Style.LinesBefore }
    if ($Style.LinesAfter -gt 0) { $params['LinesAfter'] = $Style.LinesAfter }
    if ($Style.Bold) { $params['Bold'] = $true }
    if ($Style.Italic) { $params['Italic'] = $true }
    if ($Style.Underline) { $params['Underline'] = $true }
    if ($Style.Blink) { $params['Blink'] = $true }
    if ($Style.Faint) { $params['Faint'] = $true }
    if ($Style.CrossedOut) { $params['CrossedOut'] = $true }
    if ($Style.DoubleUnderline) { $params['DoubleUnderline'] = $true }
    if ($Style.Overline) { $params['Overline'] = $true }
    if ($Style.ShowTime) { $params['ShowTime'] = $true }
    if ($Style.NoNewLine) { $params['NoNewLine'] = $true }
    if ($Style.HorizontalCenter) { $params['HorizontalCenter'] = $true }
    if ($Style.AutoPad -gt 0) { $params['AutoPad'] = $Style.AutoPad }
    if ($Style.PadLeft) { $params['PadLeft'] = $true }
    if ($Style.PadChar -ne ' ') { $params['PadChar'] = $Style.PadChar }
    if ($Style.Reverse) { $params['Reverse'] = $true }
    if ($null -ne $Style.UnderlineColor) { $params['UnderlineColor'] = $Style.UnderlineColor }
    if ($Style.UnderlineStyle) { $params['UnderlineStyle'] = $Style.UnderlineStyle }
    if ($Style.PadCenter) { $params['PadCenter'] = $true }
    if ($Style.Truncate) { $params['Truncate'] = $true }
    if ($Style.Wrap) { $params['Wrap'] = $true }

    return $params
}

function Copy-ColorStyle {
    <#
    .SYNOPSIS
    A copy of a style named with _Copy, property by property. Arrays are copied as well, so
    changing an element of the copy leaves the original alone.
    #>

    [CmdletBinding()]
    [OutputType([PSColorStyle])]
    param([Parameter(Mandatory)][object] $Style)

    $newStyle = [PSColorStyle]::new($Style.Name + '_Copy', (Copy-ColorStyleValue -Value $Style.ForegroundColor), (Copy-ColorStyleValue -Value $Style.BackgroundColor))
    $newStyle.Gradient = Copy-ColorStyleValue -Value $Style.Gradient
    $newStyle.Style = Copy-ColorStyleValue -Value $Style.Style
    $newStyle.StartTab = $Style.StartTab
    $newStyle.StartSpaces = $Style.StartSpaces
    $newStyle.LinesBefore = $Style.LinesBefore
    $newStyle.LinesAfter = $Style.LinesAfter
    $newStyle.Bold = $Style.Bold
    $newStyle.Italic = $Style.Italic
    $newStyle.Underline = $Style.Underline
    $newStyle.Blink = $Style.Blink
    $newStyle.Faint = $Style.Faint
    $newStyle.CrossedOut = $Style.CrossedOut
    $newStyle.DoubleUnderline = $Style.DoubleUnderline
    $newStyle.Overline = $Style.Overline
    $newStyle.ShowTime = $Style.ShowTime
    $newStyle.NoNewLine = $Style.NoNewLine
    $newStyle.HorizontalCenter = $Style.HorizontalCenter
    $newStyle.AutoPad = $Style.AutoPad
    $newStyle.PadLeft = $Style.PadLeft
    $newStyle.PadChar = $Style.PadChar
    $newStyle.BackgroundGradient = Copy-ColorStyleValue -Value $Style.BackgroundGradient
    $newStyle.GradientSpace = $Style.GradientSpace
    $newStyle.GradientHue = $Style.GradientHue
    $newStyle.GradientEasing = $Style.GradientEasing
    $newStyle.GradientRepeat = $Style.GradientRepeat
    $newStyle.GradientDirection = $Style.GradientDirection
    $newStyle.GradientSteps = $Style.GradientSteps
    $newStyle.Reverse = $Style.Reverse
    $newStyle.UnderlineColor = Copy-ColorStyleValue -Value $Style.UnderlineColor
    $newStyle.UnderlineStyle = $Style.UnderlineStyle
    $newStyle.PadCenter = $Style.PadCenter
    $newStyle.Truncate = $Style.Truncate
    $newStyle.Wrap = $Style.Wrap
    return $newStyle
}

function Test-ColorStyleGradient {
    <#
    .SYNOPSIS
    Whether a style's gradient is one Write-ColorEX draws: two colors or more, or the name of a
    named gradient, which the module's table resolves.
    #>

    [CmdletBinding()]
    [OutputType([bool])]
    param([AllowNull()][object[]] $Value)

    if ($null -eq $Value) {
        return $false
    }
    if ($Value.Count -ge 2) {
        return $true
    }
    return $Value.Count -eq 1 -and $Value[0] -is [string] -and $null -ne $script:GradientNames -and $script:GradientNames.Contains([string]$Value[0])
}

function Copy-ColorStyleValue {
    <#
    .SYNOPSIS
    A copy of an array, and of the arrays in it such as RGB colors; any other value as it is.
    #>

    [CmdletBinding()]
    [OutputType([object], [object[]])]
    param([AllowNull()][object] $Value)

    if ($Value -isnot [array]) {
        return $Value
    }
    $copy = $Value.Clone()
    for ($i = 0; $i -lt $copy.Length; $i++) {
        if ($copy[$i] -is [array]) {
            $copy[$i] = $copy[$i].Clone()
        }
    }
    return , $copy
}