src/typesystem/TypeManager.ps1

# Copyright 2020, Adam Edwards
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

. (import-script TypeSchema)
. (import-script TypeProvider)
. (import-script ScalarTypeProvider)
. (import-script CompositeTypeProvider)
. (import-script TypeDefinition)
. (import-script GraphObjectBuilder)

ScriptClass TypeManager {
    . {}.module.newboundscriptblock($::.TypeSchema.EnumScript)

    $graph = $null
    $definitions = $null
    $prototypes = $null
    $hasRequiredTypeDefinitions = $false

    function __initialize($graph) {
        $this.graph = $graph
        $this.definitions = @{}
        $this.prototypes = @{
            $false = @{}
            $true = @{}
        }
    }

    function GetPrototype($typeClass, $typeName, $fullyQualified = $false, $setDefaultValues = $false, $recursive = $false, $propertyFilter, [object[]] $valueList, $propertyList, $skipPropertyCheck) {
        $typeId = GetOptionallyQualifiedName $typeClass $typeName $fullyQualified
        $hasProperties = $propertyFilter -ne $null -or $propertyList -ne $null

        $prototype = if ( ! $hasProperties ) {
            GetPrototypeFromCache $typeId $setDefaultValues $recursive
        }

        if ( $hasProperties -or ! ( HasCacheKey $typeId $setDefaultValues $recursive ) ) {
            if ( ! $prototype ) {
                $type = FindTypeDefinition $typeClass $typeId $true $true
                $builder = new-so GraphObjectBuilder $this $type $setDefaultValues $recursive $propertyFilter $valueList $propertyList $skipPropertyCheck
                $prototype = $builder |=> ToObject
            }

            if ( ! $hasProperties ) {
                AddPrototypeToCache $typeId $setDefaultValues $recursive $prototype
            }
        }
        $prototype
    }

    function FindTypeDefinition($typeClass, $typeName, $fullyQualified, $errorIfNotFound = $false) {
        $definition = $null

        $classes = if ( $typeClass -eq 'Unknown' ) {
            GetTypeClassPrecedence
        } else {
            [GraphTypeClass] $typeClass
        }

        foreach ( $class in $classes ) {
            $typeId = GetOptionallyQualifiedName $class $typeName $fullyQualified

            $definition = $this.definitions[$typeId]

            if ( ! $definition ) {
                try {
                    $definition = GetTypeDefinition $class $typeId
                } catch {
                    if ( $errorIfNotFound ) {
                        throw
                    }
                }
            }

            if ( $definition ) {
                break
            }
        }

        if ( $errorIfNotFound -and ! $definition ) {
            throw "Unable to find type '$typeId' of type class '$typeClass'"
        }

        $definition
    }

    function GetTypeDefinition($typeClass, $typeId, $skipRequiredTypes) {
        $definition = $this.definitions[$typeId]

        if ( ! $definition ) {
            if ( ! $skipRequiredTypes ) {
                InitializeRequiredTypes
            }

            $type = $::.TypeDefinition |=> Get $this.graph $typeClass $typeId

            $requiredTypes = @($type)

            $baseTypeId = $type.BaseType

            while ( $baseTypeId ) {
                $baseType = $::.TypeDefinition |=> Get $this.graph Unknown $baseTypeId
                $requiredTypes += $baseType

                $baseTypeId = if ( $baseType | gm BaseType -erroraction ignore ) {
                    $basetype.BaseType
                }
            }

            for ( $typeIndex = $requiredTypes.length - 1; $typeIndex -ge 0; $typeIndex-- ) {
                $requiredType = $requiredTypes[$typeIndex]
                $requiredTypeId = $requiredType.typeId
                if ( ! $this.definitions[$requiredTypeId] ) {
                    AddTypeDefinition $requiredTypeId $requiredType
                }
            }

            $definition = $this.definitions[$typeId]
        }

        $definition
    }

    function GetPrototypeId($typeId, $setDefaults, $recursive) {
        '{0}:{1}:{2}' -f $typeId, ([int32] $setDefaults), ([int32] $recursive)
    }

    function GetPrototypeFromCache($typeId, $setDefaults, $recursive) {
        $id = GetPrototypeId $typeId $setDefaults $recursive
        $this.prototypes[$id]
    }

    function AddPrototypeToCache($typeId, $setDefaults, $recursive, $prototype) {
        $id = GetPrototypeId $typeId $setDefaults $recursive
        $this.prototypes.add($id, $prototype)
    }

    function HasCacheKey($typeId, $setDefaults, $recursive) {
        $id = GetPrototypeId $typeId $setDefaults $recursive
        $this.prototypes.ContainsKey($id)
    }

    function AddTypeDefinition($typeId, $type) {
        if ( $this.definitions[$typeId] ) {
            throw "Type '$typeId' already exists"
        }

        $this.definitions.Add($typeId, $type)
    }

    function InitializeRequiredTypes {
        if ( ! $this.hasRequiredTypeDefinitions ) {
            $requiredTypeInfo = $::.TypeProvider |=> GetRequiredTypeInfo

            $requiredTypeInfo | foreach {
                GetTypeDefinition $requiredTypeInfo.typeClass $requiredTypeInfo.typeId $true | out-null
            }

            $this.hasRequiredTypeDefinitions = $true
        }
    }

    function GetTypeClassPrecedence {
        [GraphTypeClass]::Primitive, [GraphTypeClass]::Entity, [GraphTypeClass]::Complex, [GraphTypeClass]::Enumeration
    }

    function GetOptionallyQualifiedName($typeClass, $typeName, $isFullyQualified) {
        if ( $isFullyQualified ) {
            $typeName
        } else {
            $typeNamespace = $::.TypeProvider |=> GetDefaultNamespace $typeClass $this.graph
            $::.TypeSchema |=> GetQualifiedTypeName $typeNamespace $typeName
        }
    }

    static {
        $managerByGraph = @{}

        function Get($graph) {
            $graphId = $graph |=> GetScriptObjectHashCode
            $manager = $managerByGraph[$graphId]

            if ( ! $manager ) {
                $manager = new-so TypeManager $graph
                $managerByGraph[$graphId] = $manager
            }

            $manager
        }
    }
}