### ----------------------------------------------------
### -- LibXML::Class
### -- Licenses: Artistic-2.0
### -- Authors: Vadim Belman
### -- File: lib/LibXML/Class.rakumod
### ----------------------------------------------------
### -- Chunk 1 of 3
use v6.e.PREVIEW;
unit module LibXML::Class:ver($?DISTRIBUTION.meta<ver>):auth($?DISTRIBUTION.meta<auth>):api($?DISTRIBUTION.meta<api>);
use nqp;
use experimental :will-complain;

INIT {
    PROCESS::<$LIBXML-CLASS-CTX> := Nil;
}

use AttrX::Mooish:ver<1.0.0+>:api<1.0.0+>;
use AttrX::Mooish::Attribute;
use Hash::Merge:ver<2.0.0>:auth<github:scriptkitties>:api<2>;
use LibXML::Document;
use LibXML::Element;
use LibXML::Item;
use LibXML::Namespace;
use LibXML::Text;

use LibXML::Class::Attr;
use LibXML::Class::Config :types;
use LibXML::Class::CX;
use LibXML::Class::Document;
use LibXML::Class::HOW::Element;
use LibXML::Class::HOW::ElementRole;
use LibXML::Class::HOW::ElementSeq;
use LibXML::Class::ItemDescriptor;
use LibXML::Class::Node;
use LibXML::Class::NS;
use LibXML::Class::X;
use LibXML::Class::XML;
use LibXML::Class::Types :ALL;
use LibXML::Class::Utils;

role XMLRepresentation {...}
role XMLSequential {...}

my class DeserializingCtx does LibXML::Class::NS {
    has XMLRepresentation:U $.into is required;
    has XMLRepresentation:U $.into-xml-class = $!into.xml-class;
    has LibXML::Element:D $.elem is required;
    has LibXML::Class::Document:D $.document is required;
    has LibXML::Class::Config:D $.config = $!document.config;
    has LibXML::Node @.child-elems = $!elem.childNodes;
    has Map $!child-idx = @!child-elems.map(*.unique-key).antipairs.Map;
    has LibXML::Node @.attributes = $!elem.properties;
    has Map $!attr-idx = @!attributes.map(*.unique-key).antipairs.Map;
    # User input to from-xml method
    has %.user-profile;
    # The resulting profile
    has %.profile = :$!document;
    # List of unique-key => value pairs to be later relocated into object's deserialization registry.
    has Pair:D @.deserializations;

    # Map namespace/node name into descriptor objects
    has %!xml-props;
    has %!xml-tags;
    # Target attribute for #text
    has $.xml-text;

    has $!checked-up = False;

    submethod TWEAK {
        without %!profile<xml-namespaces> {
            %!profile<xml-namespaces> =
                |$!into-xml-class.HOW.xml-namespaces,
                |$!elem.namespaces.grep(*.declaredPrefix).map({ .declaredPrefix => .declaredURI });
        }

        # Build context-dependent mappings of namespace/XML node name into Raku class attribute objects
        for $!into.xml-class.^xml-attrs(:!local).values {
            when LibXML::Class::Attr::XMLAttribute {
                self!add-attribute($_, .xml-name, %!xml-props);
            }
            when LibXML::Class::Attr::XMLValueElement {
                self!add-attribute($_, .outer-name, %!xml-tags);
            }
            when LibXML::Class::Attr::XMLTextNode {
                LibXML::Class::X::AttrDuplication::Text.new(:type($!into), :attrs($!xml-text, $_)) if $!xml-text;
                $!xml-text = $_;
            }
            default {
                die "Don't know how to use attribute's { .name } object of type " ~ .^name;
            }
        }

        if $!into ~~ XMLSequential {
            for $!into-xml-class.^xml-all-item-descriptors -> LibXML::Class::ItemDescriptor:D $desc {
                self!add-seq-descriptor($desc)
            }
        }
    }

    # Resolve current context expected default namespace based on:
    # 1. default prefix if defined
    # 2. default namespace
    # 3. a fallback found by resolving empty prefix
    method default-ns {
        self.resolve-ns( $!xml-default-ns, $!xml-default-ns-pfx, $!xml-namespaces{""} // "" )
    }

    # Get what a namespace resolves into for a particular entity given that prefix weigh upon default namespace
    method resolve-ns(Str $namespace, Str $prefix, Str:D $fallback-ns = "") {
        ($prefix
            andthen (
                $!xml-namespaces{$_}
                // fail LibXML::Class::X::NS::Prefix.new(
                    :$prefix,
                    :while("deserializing element <" ~ $.elem.localName ~ ">") )))
            // $namespace
            // $fallback-ns
    }

    method !add-tag(LibXML::Class::Descriptor:D $descriptor, $namespace is copy, $prefix, $name, %into is raw) {
        if $prefix {
            # Pick by prefix from the attribute if there is any defined
            ($namespace = $descriptor.xml-namespaces{$prefix} // $!xml-namespaces{$prefix})
                // LibXML::Class::X::NS::Prefix.new(
                    :what("type " ~ $!into.^name),
                    :while("resolving " ~ $descriptor.descriptor-kind),
                    :prefix($prefix) ).throw
        }

        without $namespace {
            # If we still don't know our namespace then it means it would be borrowed from the upstream xmlns=
            $namespace = $!xml-namespaces{""} // "";
        }

        with %into{$namespace}{$name} {
            # Some other attribute has already claimed this node under the same namespace
            LibXML::Class::X::Deserialize::DuplicateTag.new(
                :type($!into), :$namespace, :$name, :desc1($_), :desc2($descriptor) ).throw
        }
        %into{$namespace}{$name} = $descriptor;

        $namespace
    }

    method !add-attribute($attr, $name, %into is raw) {
        my ($namespace, $prefix) =
            $attr.compose-ns(
                :from($!elem),
                :default-ns($!xml-default-ns),
                :default-pfx($!xml-default-ns-pfx) );

        self!add-tag($attr, $namespace, $prefix, $name, %into);
    }

    method !add-seq-descriptor($desc) {
        my ($namespace, $prefix) =
            $desc.infer-ns( :from($!into-xml-class), :default-ns($!xml-default-ns), :default-pfx($!xml-default-ns-pfx) );

        unless my $name = $desc.xml-name {
            if $!into-xml-class.^xml-either-any {
                $name = .xml-name
                    with $.config.ns-map-type( $desc.type,
                                               :namespace(self.resolve-ns($namespace, $prefix)) );
            }
        }

        # No name means no way to resove this descriptor. This could be intentional, say, when a type is not supposed
        # to be used with sequence for a specific namespace.
        return unless $name;

        self!add-tag($desc, $namespace, $prefix, $name, %!xml-tags);
    }

    # attr-for- methods map a LibXML::Node into a LibXML::Class attribute based on the node namespace.
    # The matching NS is passed into &with callback for later use.
    method attr-for-prop(LibXML::Attr:D $desc) {
        my $namespace = $desc.namespaceURI // "";
        my $xml-name = $desc.name;
        (%!xml-props{$namespace} andthen .{$xml-name}) orelse Nil
    }

    method add-deserialization(::?CLASS:D: LibXML::Node:D $node, Mu \value) is raw {
        # note "+ DCTX deserialization for ", $node.^name, " ", brief-node-str($node), " -> ", value.raku;
        @!deserializations.push: $node => value
            if $!config.deserialization-registry;
        value
    }

    # To be used when the actual relocation happens.
    method take-out-deserializations {
        my @d = @!deserializations;
        @!deserializations = Empty;
        @d
    }

    method desc-for-elem(::?CLASS:D: LibXML::Element:D $node) {
        my $namespace = $node.namespaceURI // "";
        my $elem-name = $node.localname;
        (%!xml-tags{$namespace} andthen .{$elem-name}) orelse Nil
    }

    method unclaimed-children {
        @!child-elems.grep(*.defined)
    }

    method claim-child(LibXML::Node:D $node, LibXML::Class::Attr::XMLish :$lazy-attr) {
        @!child-elems[$!child-idx{$node.unique-key}]:delete;
    }

    method add-lazy(LibXML::Class::Attr::XMLish:D $lazy-attr, $initializer) {
        my $attr-name = $lazy-attr.name;
        # If the destination attribute is a positional we may expect more children to come later unless it is
        # containerized. For text nodes there is even no container, we always anticipate more to come.
        if $lazy-attr ~~ LibXML::Class::Attr::XMLPositional | LibXML::Class::Attr::XMLTextNode {
            (%!profile<xml-lazies>{$attr-name} //= []).push: $initializer;
        }
        else {
            if %!profile<xml-lazies>{$attr-name}:exists {
                $!config.alert:
                    LibXML::Class::X::AttrDuplication::XMLNode.new(
                        :node-name($initializer.name),
                        :attr($lazy-attr),
                        :type($!into))
            }
            %!profile<xml-lazies>{$attr-name} := $initializer;
        }
    }

    method unclaimed-attrs {
        @!attributes.grep(*.defined)
    }

    method claim-attr(LibXML::Attr:D $attr) {
        @!attributes[$!attr-idx{$attr.unique-key}]:delete
    }

    proto method to-profile(|) {*}

    multi method to-profile(LibXML::Class::Attr::XMLish:D $attr, Mu \value, *%c --> Nil) {
        self.to-profile: $attr.name.substr(2), value, positional => ($attr ~~ LibXML::Class::Attr::XMLPositional), |%c, :$attr;
    }

    multi method to-profile(Str:D $pkey,
                            Mu \value,
                            LibXML::Node :$node,
                            Bool :$positional,
                            LibXML::Class::Attr::XMLish :$attr
        --> Nil)
    {
        if $positional {
            (%!profile{$pkey} // (%!profile{$pkey} := [])).push: value;
        }
        else {
            if $node.defined && %!profile.EXISTS-KEY($pkey) {
                $!config.alert:
                    LibXML::Class::X::AttrDuplication::Node.new(:$node, :type($!into), :$attr)
            }
            %!profile{$pkey} := value;
        }
    }

    method check-up {
        return if $!checked-up;

        if @!deserializations {
            warn "Deserialization context is dropped, but deserializations of the following elements remain unrequested:\n"
                ~ @!deserializations.map({ "  " ~ brief-node-str(.key) }).join("\n");
        }

        $!checked-up = True;
    }

    method final-profile(--> Hash:D) is raw {
        my @unclaimed-elems = self.unclaimed-children;
        my @unclaimed-attrs = self.unclaimed-attrs;

        if $!config.severity !== EASY && (@unclaimed-elems || @unclaimed-attrs) {
            my $ex = LibXML::Class::X::UnclaimedNodes.new(:$!elem, :unclaimed(|@unclaimed-elems,
                                                                              |@unclaimed-attrs));
            $ex.throw if $!config.severity == STRICT;
            warn $ex.message;
        }

        if (|@unclaimed-attrs.map(*.ast), |@unclaimed-elems.map(*.ast)).List -> $unused {
            %!profile<xml-unused> := $unused
        }
        # Preserve for lazy object creation. Could be optimized out at some point by analyzing if any laziness is
        # ever expected.
        %!profile<xml-user-profile xml-default-ns xml-default-ns-pfx
                  xml-default-ns-pfx xml-unique-key xml-document> =
            %!user-profile, $!xml-default-ns, $!xml-default-ns-pfx,
            $!elem.prefix, $!elem.unique-key, $!document;

        if %!profile<xml-seq-elems> || %!profile<xml-lazies> {
            %!profile<xml-backing xml-dctx> = $!elem, self;
        }

        if @!deserializations {
            # note "@@@ WE HAVE DESERIALIZATIONS for ", $!into.^name;
            %!profile<XML-DESERIALIZATION-INVENTORY> = self.take-out-deserializations;
        }

        # Manually apply user profile to preserve key containerization.
        %!profile{$_} := %!user-profile{$_} for %!user-profile.keys;

        # Signal that the profile is an outcome of deserialization process.
        %!profile<XML-DESERIALIZED> = True;

        %!profile
    }

    submethod DESTROY {
        self.check-up;
    }
}

class XMLObject does LibXML::Class::Node does LibXML::Class::XML does LibXML::Class::Types::CloneFrom {
    has Int:D $!xml-id = LibXML::Class::Utils::next-id;

    # Collection of AST nodes representing XML entities which do not have mapping into our class
    has $!xml-unused is mooish(:lazy(-> $, *% { [] }));

    # The XML elemnt this object has been initialized from. Only makes sense for lazies.
    has LibXML::Element $!xml-backing;

    # If there is any lazy then this is the context to use to deserialize them.
    has DeserializingCtx $!xml-dctx;

    # Attribute name -> child node index on this class' element
    has Associative $!xml-lazies;

    has LibXML::Class::Document $!xml-document;

    # What we've got as from-xml %profile argument
    has %!xml-user-profile;

    # If deserialized then this would be the original element's value of .unique-key
    has Str $!xml-unique-key;

    has Array[Mu] %!xml-deserializations;

    # Setup some attributes explicitly because keeping them private is beneficial in certain cases like testing by
    # comparing objects where content of these is irrelevant.
    submethod TWEAK( Positional :$!xml-unused,
                     LibXML::Element :$!xml-backing,
                     Associative :$!xml-lazies,
                     LibXML::Class::Document :$!xml-document,
                     DeserializingCtx :$!xml-dctx,
                     Bool:D :$XML-DESERIALIZED = False,
                     Str :$!xml-unique-key,
                     :@XML-DESERIALIZATION-INVENTORY,
                     *%profile )
    {
        given self.xml-class.HOW {
            # Don't set namespace or prefix either if profile has been produced by deserialization; or if there is
            # user-provided value, even if it is undefined.
            unless $XML-DESERIALIZED {
                $!xml-default-ns //= .xml-default-ns unless %profile<xml-default-ns>:exists;
                $!xml-default-ns-pfx //= .xml-default-ns-pfx unless %profile<xml-default-ns-pfx>:exists;
            }
            # Defaults from HOW must not override what's set by the user. Deserialization might override these from
            # XML source.
            merge-in-namespaces(self.xml-namespaces, .xml-namespaces);
        }
        self!MAYBE-REGISTER-ON-DOCUMENT;

        if @XML-DESERIALIZATION-INVENTORY {
            for @XML-DESERIALIZATION-INVENTORY {
                # note "%%% TWEAK ADD TO DESER: ", .key, " => ", .value.raku,
                #     |(
                #         "\n   unique key: ", .key.unique-key if .key ~~ LibXML::Node
                #     );
                self.xml-add-deserialization( .key, .value )
            }
        }
    }

    method !MAYBE-REGISTER-ON-DOCUMENT {
        $!xml-document.add-deserialization(self) with $!xml-unique-key;
    }

    method clone(::?CLASS:D: *%twiddles) is raw {
        my %profile;
        # We only need to take care of lazified xml-element or xml-attribute attributes which already received their values.
        for self.xml-class.^xml-attrs(:!local).values -> LibXML::Class::Attr::Node $attr-desc {
            my $attr = $attr-desc.attr;
            if $attr ~~ AttrX::Mooish::Attribute && $attr.is-set(self) {
                %profile{$attr.base-name} := $attr.get_value(self);
            }
        }
        # Make a copy of xml-lazies to make it independent from the original object. Otherwise any modifications to the
        # hash would be shared causing initialization of an attribute on any of the two objects resulting in no element
        # found for initialization on another.
        %profile<xml-lazies> := $!xml-lazies.clone;
        %profile<xml-id> = LibXML::Class::Utils::next-id;
        my $cloned := callwith(|%profile, |%twiddles);
        $cloned.post-clone(|%profile, |%twiddles);
        $cloned
    }

    method post-clone(Associative :$!xml-lazies, UInt:D :$!xml-id) {
        # note self.^name, " is about to register on document";
        self!MAYBE-REGISTER-ON-DOCUMENT;
    }

    method xml-config {
        $*LIBXML-CLASS-CONFIG // $!xml-document.config // LibXML::Class::Config.global
    }

    # For some purposes it's better for the attributes to remain private, see TWEAK's comment.
    method xml-dctx(::?CLASS:D:)         { $!xml-dctx         }
    method xml-unused(::?CLASS:D:)       { $!xml-unused       }
    method xml-backing(::?CLASS:D:)      { $!xml-backing      }
    method xml-lazies(::?CLASS:D:)       { $!xml-lazies       }
    method xml-document(::?CLASS:D:)     { $!xml-document     }
    method xml-user-profile(::?CLASS:D:) { %!xml-user-profile }
    method xml-unique-key(::?CLASS:D:)   { $!xml-unique-key   }
    method xml-id(::?CLASS:D:)           { $!xml-id           }

    method xml-has-lazies(::?CLASS:D:)   { ? $!xml-lazies     }

    method xml-serialize-stages is raw {
        ('xml-to-element-repr',)
    }

    method xml-profile-stages is raw {
        ('xml-from-element-repr',)
    }

    # Overriding this method can help in using from-xml method to de-serialize into non-XMLObject classes. See, for
    # example, method xmlize of LibXML::Class::Config
    method xml-create(*%profile) {
        self.new: |%profile
    }

    method xml-add-deserialization(::?CLASS:D: LibXML::Node:D $node, Mu \repr --> Mu) {
        if $.xml-config.deserialization-registry {
            %!xml-deserializations{$node.unique-key}.push: repr;
        }
        repr
    }

    proto method xml-deserializations(::?CLASS:D: $ --> Mu) {*}
    multi method xml-deserializations(::?CLASS:D: LibXML::Node:D $node --> Positional) is raw {
        my $unique-key = $node.unique-key;
        %!xml-deserializations.EXISTS-KEY($unique-key) ?? %!xml-deserializations.AT-KEY($unique-key) !! Nil
    }
    multi method xml-deserializations(::?CLASS:D: Str:D $key --> Positional) is raw {
        %!xml-deserializations.EXISTS-KEY($key) ?? %!xml-deserializations.AT-KEY($key) !! Nil
    }

    proto method xml-has-deserialization(::?CLASS:D: $) {*}
    multi method xml-has-deserialization(::?CLASS:D: LibXML::Node:D $node) {
        %!xml-deserializations.EXISTS-KEY{$node.unique-key}
    }
    multi method xml-has-deserialization(::?CLASS:D: Str:D $key) {
        %!xml-deserializations.EXISTS-KEY{$key}
    }

    proto method xml-remove-deserialization(::?CLASS:D: $) {*}
    multi method xml-remove-deserialization(::?CLASS:D: ::?CLASS:D \repr --> Nil) {
        nextwith repr.unique-key, repr
    }
    multi method xml-remove-deserialization(::?CLASS:D: Str:D $key, Mu \repr --> Nil) {
        return unless %!xml-deserializations.EXISTS-KEY($key);
        %!xml-deserializations{$key} .= grep({ $_ !=== repr });
    }

    # If there is a lazy attribute for this node then deserialize it.
    # When :resolve-as-object late resolution entries, like container nodes, would return their owner object (us)
    # instead of descriptor's attribute value.
    method xml-find-deserializations(::?CLASS:D: LibXML::Node:D $node, Bool :$resolve-as-object = False --> Iterable) {
        my $unique-key = $node.unique-key;
        # note "? . find deserialization for ", brief-node-str($node);
        if %!xml-deserializations.EXISTS-KEY($unique-key) {
            my proto sub resolve-ds(|) {*}
            multi sub resolve-ds(LibXML::Class::Attr::XMLish:D $desc) {
                # If deserialization is registered as an attribute descriptor then update it with actual attribute's value.
                # note "? . resolve as descriptor ", $desc, ", as object? ", $resolve-as-object;
                my $attr = $desc.attr;
                if $resolve-as-object {
                    if $attr ~~ AttrX::Mooish::Attribute && !$attr.is-set(self) {
                        # If attribute has been lazified then we may need to vivify it before returning. This is
                        # necessary for XML-containerized attributes in order to get container child nodes registered
                        # with this object.
                        my $ := $attr.get_value(self).raku;
                    }
                    return self
                }
                $attr.get_value(self)
            }
            multi sub resolve-ds(\value) {
                # note "? . resolve as plain value";
                value
            }
            return %!xml-deserializations{$unique-key}.map: &resolve-ds
        }

        # Don't even try deserializing if there is no context. It means we're not lazy (never was or everything is
        # already deserialized). xml-deserialize-node does this check too, but let's optimize a bit here.
        without $!xml-dctx {
            fail LibXML::Class::X::Deserialize::NoCtx.new(:type(self.WHAT), :node($node))
        }

        (self.xml-deserialize-node($node),)
    }

    method xml-findnodes(::?CLASS:D: |c) {
        fail LibXML::Class::X::Deserialize::NoBacking.new(:type(self.WHAT), :what("use 'xml-findnodes' methods"))
            without $!xml-backing;
        # Use document's find-deserializations because the query might produce nodes from anywhere in the XML document.
        $!xml-backing.findnodes(|c).map: { |$!xml-document.find-deserializations($_) }
    }

    method xml-create-child-element( ::?CLASS:D:
                                     LibXML::Element:D $parent,
                                     LibXML::Class::Node:D $from,
                                     Str :$name,
                                     *%profile
        --> LibXML::Element:D )
    {
        # By default we borrow parent element default namespace.
        my $child = $parent.ownerDocument.createElement($name // $from.xml-name);
        $parent.add: $child;
        $from.xml-apply-ns($child, |%profile);
        $child
    }

    proto method xml-type-from-str(Mu:U, Str:D) {*}
    multi method xml-type-from-str(Bool, Str:D $xml-value) {
        ?($xml-value eq ("1" | "true"))
            or ($xml-value eq ("0" | "false")
                ?? False
                !! LibXML::Class::X::Deserialize::BadValue.new(:type(Bool), :value($xml-value)).throw)
    }
    multi method xml-type-from-str(::T Mu:U, T(Str:D) \coerced) {
        coerced
    }

    proto method xml-type-to-str(|) {*}
    multi method xml-type-to-str(Bool:D \val) {
        val.Str.lc
    }
    # Make sure Num is still recognized as a Num by force-preserve the exponent part 'e' and trying to preserve all
    # fraction digits. Since there is no fully legitimate way to do it then we use a trick by printing more that it is
    # really necessary and then stripping off all tailing zeroes.
    multi method xml-type-to-str(Num:D \val) {
        S/^ $<significant>=[.* "." . .*?] 0* $<exponential>=[ e <[+-]>? \d+ ] $/$<significant>$<exponential>/
            with val.fmt('%.64e')
    }
    multi method xml-type-to-str(Mu:D \val where *.HOW ~~ Metamodel::EnumHOW) {
        ~val.value
    }
    multi method xml-type-to-str(Mu \val) {
        ~val
    }

    proto method xml-try-serializer(::?CLASS:D: |) {*}
    multi method xml-try-serializer( ::?CLASS:D:
                                     LibXML::Class::Descriptor:D $desc,
                                     Mu \value,
                                     &fallback
                                     --> Mu ) is raw
    {
        self.xml-try-deserializer: $desc, \(value), &fallback
    }
    multi method xml-try-serializer( ::?CLASS:D:
                                     LibXML::Class::Descriptor:D $desc,
                                     LibXML::Node:D \xml-node,
                                     Mu \value,
                                     &fallback
                                     --> Mu ) is raw
    {
        self.xml-try-deserializer: $desc, \( xml-node, value ), &fallback
    }
    multi method xml-try-serializer( ::?CLASS:D:
                               LibXML::Class::Descriptor:D $desc,
                               Capture:D \serializer-args,
                               &fallback
                               --> Mu ) is raw
    {
        my Mu $rc;
        my $use-fallback = True;

        if $desc.serializer-cando(serializer-args) {
            # Whatever happens now we must not use fallback except when $desc.serializer explicitly calls xml-I-cant
            $use-fallback = False;

            $rc := try {
                CATCH { default { return .Failure } }
                CONTROL {
                    when LibXML::Class::CX::Cannot {
                        # User code chosen not to serialize.
                        $use-fallback = True;
                    }
                    default { .rethrow }
                }
                $desc.serializer.(|serializer-args)
            }
        }

        $use-fallback ?? &fallback() !! $rc
    }

    my subset DeserializableSource of Any where LibXML::Node | Str;

    method xml-try-deserializer( LibXML::Class::Descriptor:D $desc,
                                 DeserializableSource:D $source,
                                 &fallback?,
                                 Mu :$value-type is raw = NOT-SET,
                                 DeserializingCtx :$dctx,
                                 Bool:D :$coerce = True
                                 --> Mu ) is raw
    {
        my Mu $rc;
        my \expect-type = $value-type =:= NOT-SET ?? $desc.value-type !! $value-type;

        my $value := do given $source {
            when LibXML::Attr { .value  }
            when LibXML::Text { .data   }
            when LibXML::Node { Nil     }
            default           { $source }
        }

        # note "VALUE TYPE of ", $desc.descriptor-kind, " is ", expect-type.^name, "\n",
        #     "    deserializing from ", $value.WHICH, "\n",
        #     "    can deserialize? ", $desc.deserializer-cando($value);

        my Bool $use-type-to-str = !$desc.has-deserializer;

        unless $use-type-to-str {
            my $what =
                $desc.deserializer-cando($source)
                    ?? $source
                    !! ($value andthen $desc.deserializer-cando($_))
                        ?? $value
                        !! Nil;
            with $what {
                $rc := try {
                    CATCH { default { return .Failure } }
                    CONTROL {
                        when LibXML::Class::CX::Cannot {
                            $use-type-to-str = True;
                        }
                        default { .rethrow }
                    }
                    $desc.deserializer.($what)
                }
            }
            else {
                $use-type-to-str = True;
            }
        }

        if $use-type-to-str {
            $rc := &fallback
                    ?? &fallback()
                    !! $coerce
                        ?? self.xml-type-from-str: expect-type, $value
                        !! Nil;
        }

        # If our source is a LibXML::Node then try registering its deserialization. Otherwise we only need the final
        # result
        if $rc !=:= Nil && $source ~~ LibXML::Node && $dctx.defined {
            $dctx.add-deserialization($source, $rc);
        }

        $rc
    }

    method !single-text-node(LibXML::Element:D $elem, Str $what?) {
        unless $elem.childNodes.elems == 1 && (my $text := $elem.head) ~~ LibXML::Text:D {
            LibXML::Class::X::Deserialize::BadNode.new(
                :type(self.WHAT),
                :expected("a simple " ~ |("$_ " with $what) ~ "XML element"),
                :got("malformed " ~ brief-node-str($elem)) ).throw
        }
        $text
    }

    # xml-coerce-into-attr always works on a single string or element. Any containerization, positionals are to be unwrapped
    # by the upstream. Since associatives are always single-elemented we do handle them in here.
    proto method xml-coerce-into-attr(LibXML::Class::Attr::XMLish:D, $) {*}

    multi method xml-coerce-into-attr(LibXML::Class::Attr::XMLAssociative:D $desc, LibXML::Element:D $node) {
        my Mu \value-type = $desc.value-type;
        my Mu \key-type = $desc.type.keyof;

        my $dctx = $*LIBXML-CLASS-CTX;

        my $value-attr = $desc.value-attr;

        # note "%%% COERCING INTO ", $desc.name, " from ", brief-node-str($node), " // ", $dctx.WHICH;

        my sub mapper-attr($_) {
            my $xml-attr = .getAttributeNode($value-attr);
            # note "%%% MAPPER ATTR for ", $xml-attr;
            $dctx.add-deserialization:
                $_,
                (self.xml-type-from-str(key-type, .localname) => self.xml-try-deserializer($desc, $xml-attr, :$dctx))
        }
        my sub mapper-text($_) {
            # note "%%% MAPPER TEXT for ", .WHICH;
            $dctx.add-deserialization:
                $_,
                (self.xml-type-from-str(key-type, .localname) =>
                    self.xml-try-deserializer($desc, self!single-text-node($_, "hash pair"), :$dctx))
        }

        my &mapper = $value-attr ?? &mapper-attr !! &mapper-text;

        $node.childNodes.map(&mapper).eager
    }

    multi method xml-coerce-into-attr(LibXML::Class::Attr::XMLValueElement:D $desc, LibXML::Element:D $node) {
        # Deserializer of an element attribute takes the element. We wash our hands here.
        # note "? trying deserializer with ", $desc.descriptor-kind;
        my $not-deserialized = False;
        my $dctx = $*LIBXML-CLASS-CTX;

        # note "??? Try deserializer on node ", $node;
        my Mu $value := self.xml-try-deserializer: $desc, $node, :!coerce, :$dctx, {
            # This is a fallback when for any reason user deserializer hasn't produced a value.
            $not-deserialized = True;
            Nil
        };
        # note "? not deserialized: ", $not-deserialized, ", value=", $value.raku;

        if $not-deserialized {
            my $xml-config = $.xml-config;

            my Mu $desc-type;
            my Mu $value-type;
            my $is-any = $desc.is-any;
            my LibXML::Element:D $velem = $node;
            my %named = :user-profile($dctx.user-profile);

            if $is-any {
                $velem = $node.elements.head;
                $desc-type := nominalize-type($value-type := $xml-config.ns-map($velem));
                if $desc-type =:= Nil {
                    my $ex = LibXML::Class::X::Deserialize::NoNSMap.new(:type(self.WHAT), :elem($velem));
                    $xml-config.alert: $ex;
                    # Unless severity level is STRICT then return a Failure. If that's ok with the user and their
                    # destination container allows it then that failure can be used by serialization to reproduce the
                    # original element. Otherwise throwing due to failed typecheck would be a totally reasonable
                    # outcome.
                    fail $ex
                }

                # Element name may differ from what's $desc-type default name is. Make sure it won't be a problem.
                %named<name> = $velem.localName;
            }
            else {
                $value-type := $desc.value-type<>;
                $desc-type := $desc.nominal-type<>;
                %named<name> = $desc.value-name;
            }

            unless $desc-type ~~ BasicType | XMLRepresentation {
                if $desc-type.^archetypes.composable {
                    # If we got here then attribute's type is a role and there is no way to find out what class to
                    # deserialize into.
                    LibXML::Class::X::Deserialize::Role.new(:type(self.WHAT), :$desc).throw
                }
                $desc-type := $xml-config.xmlize($desc-type, XMLRepresentation);
            }

            # If destination type is an XMLRepresentation then let it deserialize itself
            if $desc-type ~~ XMLRepresentation {
                # Make sure prefixes declared with this attribute xml-element are propagaded downstream.
                my %*LIBXML-CLASS-NS-OVERRIDE = $desc.xml-namespaces;
                %named<namespace prefix> =
                    $desc.compose-ns(:from($dctx.elem), :default-ns($dctx.xml-default-ns), :default-pfx($dctx.xml-default-ns-pfx));
                $value := $desc-type.from-xml($velem, $dctx.document, |%named)
            }
            else {
                # Otherwise this is a case of simple element where we need either text content or value attribute.
                my $value-attr = $desc.value-attr;
                $value :=
                    self.xml-try-deserializer:
                        $desc,
                        ( $value-attr
                            ?? $velem.getAttributeNode($value-attr)
                            !! self!single-text-node($velem) ),
                        :$value-type,
                        :$dctx;
            }

            # $velem will differ from $node in XML:any case
            $dctx.add-deserialization($velem, $value) if $is-any;
        }
        # note "--- coerced ", brief-node-str($node), " into ", $value.raku;
        # If XML:any then $value is the inner tag and we'd need to register the outer too.
        $dctx.add-deserialization($node, $value)
    }

    multi method xml-coerce-into-attr(LibXML::Class::Attr::XMLAttribute:D $desc, LibXML::Attr:D $xml-attr) {
        self.xml-try-deserializer: $desc, $xml-attr, :dctx($*LIBXML-CLASS-CTX)
    }

    multi method xml-coerce-into-attr(LibXML::Class::Attr::XMLTextNode:D $desc, Str:D $xml-value) {
        self.xml-try-deserializer: $desc, $xml-value
    }

    method xml-lazy-deserialize-context(&