# Copyrights 2008-2018 by [Mark Overmeer].
#  For other contributors see ChangeLog.
# See the manual pages for details on the licensing terms.
# Pod stripped from pm file by OODoc 2.02.
# This code is part of distribution XML-Compile.  Meta-POD processed with
# OODoc into POD and HTML manual-pages.  See README.md
# Copyright Mark Overmeer.  Licensed under the same terms as Perl itself.

package XML::Compile::Cache;
use vars '$VERSION';
$VERSION = '1.06';

use base 'XML::Compile::Schema';

use warnings;
use strict;

use Log::Report 'xml-compile-cache';

use XML::Compile::Util   qw/pack_type unpack_type/;
use List::Util           qw/first/;
use Scalar::Util         qw/weaken/;
use XML::LibXML::Simple  qw/XMLin/;


sub init($)
{   my ($self, $args) = @_;
    $self->addPrefixes($args->{prefixes});

    $self->SUPER::init($args);

    $self->{XCC_opts}   = delete $args->{opts_rw}      || [];
    $self->{XCC_ropts}  = delete $args->{opts_readers} || [];
    $self->{XCC_wopts}  = delete $args->{opts_writers} || [];
    $self->{XCC_undecl} = delete $args->{allow_undeclared} || 0;

    $self->{XCC_dropts} = {};  # declared opts
    $self->{XCC_dwopts} = {};
    $self->{XCC_uropts} = {};  # undeclared opts
    $self->{XCC_uwopts} = {};

    $self->{XCC_readers} = {}; # compiled code refs;
    $self->{XCC_writers} = {};

    $self->typemap($args->{typemap});
    $self->xsiType($args->{xsi_type});
    $self->anyElement($args->{any_element} || 'ATTEMPT');

    $self;
}

#----------------------


sub typemap(@)
{   my $self = shift;
    my $t    = $self->{XCC_typemap} ||= {};
    my @d    = @_ > 1 ? @_ : !defined $_[0] ? ()
             : ref $_[0] eq 'HASH' ? %{$_[0]} : @{$_[0]};
    while(@d) { my $k = $self->findName(shift @d); $t->{$k} = shift @d }
    $t;
}


sub addXsiType(@)
{   my $self = shift;
    my $x    = $self->{XCC_xsi_type} ||= {};
    my @d    = @_ > 1 ? @_ : !defined $_[0] ? ()
             : ref $_[0] eq 'HASH' ? %{$_[0]} : @{$_[0]};

    while(@d)
    {   my $k = $self->findName(shift @d);
        my $a = shift @d;
        $a = $self->namespaces->autoexpand_xsi_type($k) || []
            if $a eq 'AUTO';

        push @{$x->{$k}}
          , ref $a eq 'ARRAY' ? (map $self->findName($_), @$a)
          :                     $self->findName($a);
    }

    $x;
}
*xsiType = \&addXsiType;


sub allowUndeclared(;$)
{   my $self = shift;
    @_ ? ($self->{XCC_undecl} = shift) : $self->{XCC_undecl};
}


sub anyElement($)
{   my ($self, $anyelem) = @_;

    # the "$self" in XCC_ropts would create a ref-cycle, causing a
    # memory leak.
    my $s = $self; weaken $s;

    my $code
      = $anyelem eq 'ATTEMPT' ? sub {$s->_convertAnyTyped(@_)}
      : $anyelem eq 'SLOPPY'  ? sub {$s->_convertAnySloppy(@_)}
      :                         $anyelem;
     
    $self->addCompileOptions(READERS => any_element => $code);
    $code;
}

#----------------------


sub addPrefixes(@)
{   my $self  = shift;
    my $p     = $self->{XCC_namespaces} ||= {};
    my $first = shift;
    @_ or defined $first
        or return $p;

    my @pairs
      = @_                    ? ($first, @_)
      : ref $first eq 'ARRAY' ? @$first
      : ref $first eq 'HASH'  ? %$first
      : error __x"prefixes() expects list of PAIRS, an ARRAY or a HASH";

    my $a    = $self->{XCC_prefixes} ||= {};
    while(@pairs)
    {   my ($prefix, $ns) = (shift @pairs, shift @pairs);
        $p->{$ns} ||= { uri => $ns, prefix => $prefix, used => 0 };

        if(my $def = $a->{$prefix})
        {   if($def->{uri} ne $ns)
            {   error __x"prefix `{prefix}' already refers to {uri}, cannot use it for {ns}"
                  , prefix => $prefix, uri => $def->{uri}, ns => $ns;
            }
        }
        else
        {   $a->{$prefix} = $p->{$ns};
            trace "register prefix $prefix for '$ns'";
        }
    }
    $p;
}



sub prefixes(@)
{   my $self = shift;
    return $self->addPrefixes(@_) if @_;
    $self->{XCC_namespaces} || {};
}


sub prefix($) { $_[0]->{XCC_prefixes}{$_[1]} }

# [0.995] should this be public?
sub byPrefixTable() { shift->{XCC_prefixes} }


sub prefixFor($)
{   my $def = $_[0]->{XCC_namespaces}{$_[1]} or return ();
    $def->{used}++;
    $def->{prefix};
}


sub addNicePrefix($$)
{   my ($self, $base, $ns) = @_;
    if(my $def = $self->prefix($base))
    {   return $base if $def->{uri} eq $ns;
    }
    else
    {   $self->addPrefixes($base => $ns);
        return $base;
    }

    $base .= '01' if $base !~ m/[0-9]$/;
    while(my $def = $self->prefix($base))
    {   return $base if $def->{uri} eq $ns;
        $base++;
    }
    $self->addPrefixes($base => $ns);
    $base;
}


sub learnPrefixes($)
{   my ($self, $node) = @_;
    my $namespaces = $self->prefixes;

  PREFIX:
    foreach my $ns ($node->getNamespaces)  # learn preferred ns
    {   my ($prefix, $uri) = ($ns->getLocalName, $ns->getData);
        next if !defined $prefix || $namespaces->{$uri};

        if(my $def = $self->prefix($prefix))
        {   next PREFIX if $def->{uri} eq $uri;
        }
        else
        {   $self->addPrefixes($prefix => $uri);
            next PREFIX;
        }

        $prefix =~ s/0?$/0/;
        while(my $def = $self->prefix($prefix))
        {   next PREFIX if $def->{uri} eq $uri;
            $prefix++;
        }
        $self->addPrefixes($prefix => $uri);
    }
}

sub addSchemas($@)
{   my ($self, $xml) = (shift, shift);
    $self->learnPrefixes($xml);
    $self->SUPER::addSchemas($xml, @_);
}


sub prefixed($;$)
{   my $self = shift;
    my ($ns, $local) = @_==2 ? @_ : unpack_type(shift);
    $ns or return $local;
    my $prefix = $self->prefixFor($ns);
    defined $prefix
        or error __x"no prefix known for namespace `{ns}', use addPrefixes()"
            , ns => $ns;

    length $prefix ? "$prefix:$local" : $local;
}

#----------------------


sub compileAll(;$$)
{   my ($self, $need, $usens) = @_;
    my ($need_r, $need_w) = $self->_need($need || 'RW');

    if($need_r)
    {   foreach my $type (keys %{$self->{XCC_dropts}})
        {   if(defined $usens)
            {   my ($myns, $local) = unpack_type $type;
                next if $usens eq $myns;
            }
            $self->reader($type);
        }
    }

    if($need_w)
    {   foreach my $type (keys %{$self->{XCC_dwopts}})
        {   if(defined $usens)
            {   my ($myns, $local) = unpack_type $type;
                next if $usens eq $myns;
            }
            $self->writer($type);
        }
    }
}


sub _same_params($$)
{   my ($f, $s) = @_;
    @$f==@$s or return 0;
    for(my $i=0; $i<@$f; $i++)
    {   return 0 if !defined $f->[$i] ? defined $s->[$i]
                  : !defined $s->[$i] ? 1 : $f->[$i] ne $s->[$i];
    }
    1;
}

sub reader($@)
{   my ($self, $name) = (shift, shift);
    my %args    = @_;
    my $type    = $self->findName($name);
    my $readers = $self->{XCC_readers};

    if(exists $self->{XCC_dropts}{$type})
    {   trace __x"ignoring options to pre-declared reader {name}"
          , name => $name if @_;

        return $readers->{$type}
            if $readers->{$type};
    }
    elsif($self->allowUndeclared)
    {   if(my $ur = $self->{XCC_uropts}{$type})
        {   # do not use cached version when options differ
            _same_params $ur, \@_
                or return $args{is_type}
                  ? $self->compileType(READER => $type, @_)
                  : $self->compile(READER => $type, @_);
        }
        else
        {   $self->{XCC_uropts}{$type} = \@_;
        }
    }
    elsif(exists $self->{XCC_dwopts}{$type})
         { error __x"type {name} is only declared as writer", name => $name }
    else { error __x"type {name} is not declared", name => $name }

    $readers->{$type} ||= $args{is_type}
       ? $self->compileType(READER => $type, @_)
       : $self->compile(READER => $type, @_);
}


sub writer($%)
{   my ($self, $name) = (shift, shift);
    my %args    = @_;
    my $type    = $self->findName($name);
    my $writers = $self->{XCC_writers};

    if(exists $self->{XCC_dwopts}{$type})
    {   trace __x"ignoring options to pre-declared writer {name}"
          , name => $name if @_;

        return $writers->{$type}
            if $writers->{$type};
    }
    elsif($self->{XCC_undecl})
    {   if(my $ur = $self->{XCC_uwopts}{$type})
        {   # do not use cached version when options differ
            _same_params $ur, \@_
                or return $args{is_type}
                  ? $self->compileType(WRITER => $type, @_)
                  : $self->compile(WRITER => $type, @_);
        }
        else
        {   $self->{XCC_uwopts}{$type} = \@_;
        }
    }
    elsif(exists $self->{XCC_dropts}{$type})
    {   error __x"type {name} is only declared as reader", name => $name;
    }
    else
    {   error __x"type {name} is not declared", name => $name;
    }

    $writers->{$type} ||= $args{is_type}
       ? $self->compileType(WRITER => $type, @_)
       : $self->compile(WRITER => $type, @_);

}

sub template($$@)
{   my ($self, $action, $name) = (shift, shift, shift);
    $action =~ m/^[A-Z]*$/
        or error __x"missing or illegal action parameter to template()";

    my $type  = $self->findName($name);
    my @opts = $self->mergeCompileOptions($action, $type, \@_);
    $self->SUPER::template($action, $type, @opts);
}


sub addCompileOptions(@)
{   my $self = shift;
    my $need = @_%2 ? shift : 'RW';

    my $set
      = $need eq 'RW'      ? $self->{XCC_opts}
      : $need eq 'READERS' ? $self->{XCC_ropts}
      : $need eq 'WRITERS' ? $self->{XCC_wopts}
      : error __x"addCompileOptions() requires option set name, not {got}"
          , got => $need;

    if(ref $set eq 'HASH')
         { while(@_) { my $k = shift; $set->{$k} = shift } }
    else { push @$set, @_ }
    $set;
}

# Create a list with options for X::C::Schema::compile(), from a list of ARRAYs
# and HASHES with options.  The later options overrule the older, but in some
# cases, the new values are added.  This method knows how some of the options
# of ::compile() behave.  [last update X::C v0.98]

sub mergeCompileOptions($$$)
{   my ($self, $action, $type, $opts) = @_;

    my @action_opts
      = ($action eq 'READER' || $action eq 'PERL')
      ? ($self->{XCC_ropts}, $self->{XCC_dropts}{$type})
      : ($self->{XCC_wopts}, $self->{XCC_dwopts}{$type});

    my %p    = %{$self->{XCC_namespaces}};
    my %t    = %{$self->{XCC_typemap}};
    my %x    = %{$self->{XCC_xsi_type}};
    my %opts = (prefixes => \%p, hooks => [], typemap => \%t, xsi_type => \%x);

    # flatten list of parameters
    my @take = map {!defined $_ ? () : ref $_ eq 'ARRAY' ? @$_ : %$_ }
        $self->{XCC_opts}, @action_opts, $opts;

    while(@take)
    {   my ($opt, $val) = (shift @take, shift @take);
        defined $val or next;

        if($opt eq 'prefixes')
        {   my $t = $self->_namespaceTable($val, 1, 0);  # expand
            @p{keys %$t} = values %$t;   # overwrite old def if exists
        }
        elsif($opt eq 'hooks' || $opt eq 'hook')
        {   my $hooks = $self->_cleanup_hooks($val);
            unshift @{$opts{hooks}}, ref $hooks eq 'ARRAY' ? @$hooks : $hooks
                if $hooks;
        }
        elsif($opt eq 'typemap')
        {   $val ||= {};
            if(ref $val eq 'ARRAY')
            {   while(@$val)
                {   my $k = $self->findName(shift @$val); 
                    $t{$k} = shift @$val;
                }
            }
            else
            {   while(my($k, $v) = each %$val)
                {   $t{$self->findName($k)} = $v;
                }
            }
        }
        elsif($opt eq 'key_rewrite')
        {   unshift @{$opts{key_rewrite}}, ref $val eq 'ARRAY' ? @$val : $val;
        }
        elsif($opt eq 'xsi_type')
        {   while(my ($t, $a) = each %$val)
            {   my @a = ref $a eq 'ARRAY' ? map($self->findName($_), @$a)
                  : $self->findName($a);
                push @{$x{$self->findName($t)}},  @a;
            }
        }
        elsif($opt eq 'ignore_unused_tags')
        {   $opts{$opt} = defined $opts{$opt} ? qr/$opts{$opt}|$val/ : $val;
        }
        else
        {   $opts{$opt} = $val;
        }
    }

    %opts;
}

# rewrite hooks
sub _cleanup_hooks($)
{   my ($self, $hooks) = @_;
    $hooks or return;

    # translate prefixed type names into full names
    foreach my $hook (ref $hooks eq 'ARRAY' ? @$hooks : $hooks)
    {   if(my $types = $hook->{type})
        {   $hook->{type} =
              [ map {ref $_ eq 'Regexp' ? $_ : $self->findName($_)}
                       ref $types eq 'ARRAY' ? @$types : $types ];
        }
        elsif(my $ext = $hook->{extends})
        {   $hook->{extends} = $self->findName($ext);
        }
    }
    $hooks;
}

my %need = (READER => [1,0], WRITER => [0,1], RW => [1,1]);
$need{READERS} = $need{READER};
$need{WRITERS} = $need{WRITER};

sub _need($)
{   my $need = $need{$_[1]}
       or error __x"use READER, WRITER or RW, not {dir}", dir => $_[1];
    @$need;
}

# support prefixes on types
sub addHook(@)
{   my $self = shift;
    my $hook = @_ > 1 ? {@_} : shift;
    $self->_cleanup_hooks($hook);
    $self->SUPER::addHook($hook);
}

sub compile($$@)
{   my ($self, $action, $elem) = splice @_, 0, 3;
    defined $elem
        or error __x"compile() requires action and type parameters";

    $self->SUPER::compile
      ( $action => $self->findName($elem)
      , $self->mergeCompileOptions($action, $elem, \@_)
      );
}

sub compileType($$@)
{   my ($self, $action, $type) = splice @_, 0, 3;
    defined $type
        or error __x"compileType() requires action and type parameters";

    $self->SUPER::compileType
      ( $action => $self->findName($type)
      , $self->mergeCompileOptions($action, $type, \@_)
      );
}

#----------------------


sub declare($$@)
{   my ($self, $need, $names, @opts) = @_;
    my $opts = @opts==1 ? shift @opts : \@opts;
    $opts = [ %$opts ] if ref $opts eq 'HASH';

    my ($need_r, $need_w) = $self->_need($need);

    foreach my $name (ref $names eq 'ARRAY' ? @$names : $names)
    {   my $type = $self->findName($name);
        trace "declare $type $need";

        if($need_r)
        {   defined $self->{XCC_dropts}{$type}
               and warning __x"reader {name} declared again", name => $name;
            $self->{XCC_dropts}{$type} = $opts;
        }

        if($need_w)
        {   defined $self->{XCC_dwopts}{$type}
               and warning __x"writer {name} declared again", name => $name;
            $self->{XCC_dwopts}{$type} = $opts;
        }
    }

    $self;
}


sub findName($)
{   my ($self, $name) = @_;
    defined $name
        or panic "findName called without name";

    return $name
        if substr($name, 0, 1) eq '{';

    my ($prefix,$local) = $name =~ m/^([\w-]*)\:(\S*)$/ ? ($1,$2) : ('',$name);
    my $def = $self->{XCC_prefixes}{$prefix};
    unless($def)
    {   return $name if $prefix eq '';   # namespace-less
        trace __x"known prefixes: {prefixes}"
          , prefixes => [ sort keys %{$self->{XCC_prefixes}} ];
        error __x"unknown name prefix `{prefix}' for `{name}'"
           , prefix => $prefix, name => $name;
    }

    length $local ? pack_type($def->{uri}, $local) : $def->{uri};
}


sub printIndex(@)
{   my $self = shift;
    my $fh   = @_ % 2 ? shift : select;
    my %args = @_;
    my $decl = exists $args{show_declared} ? delete $args{show_declared} : 1;

    return $self->SUPER::printIndex($fh, %args)
        unless $decl;

    my $output = '';
    open my($out), '>', \$output;

    $self->SUPER::printIndex($out, %args);

    close $out;
    my @output = split /(?<=\n)/, $output;
    my $ns     = '';
    foreach (@output)
    {   $ns = $1 if m/^namespace\:\s+(\S+)/;
        my $local = m/^\s+(\S+)\s*$/ ? $1 : next;
        my $type  = pack_type $ns, $local;

        substr($_, 1, 1)
          = $self->{XCC_readers}{$type} ? 'R'
          : $self->{XCC_dropts}{$type}  ? 'r' : ' ';

        substr($_, 2, 1)
          = $self->{XCC_writers}{$type} ? 'W'
          : $self->{XCC_dwopts}{$type}  ? 'w' : ' ';
    }
    $fh->print(@output);
}

#---------------
# Convert ANY elements and attributes

sub _convertAnyTyped(@)
{   my ($self, $type, $nodes, $path, $read) = @_;

    my $key     = $read->keyRewrite($type);
	if(defined(my $blocked = $read->blocked($path, complexType => $type)))
    {   trace "skipping blocked $type";
        return ();
    }

    my $reader  = try { $self->reader($type) };
    if($@)
    {   trace "cannot auto-convert 'any': ".$@->wasFatal->message;
        return ($key => $nodes);
    }
    trace "auto-convert known type for 'any': $type";

    my @nodes   = ref $nodes eq 'ARRAY' ? @$nodes : $nodes;
    my @convert = map $reader->($_), @nodes;
    ($key => (@convert==1 ? $convert[0] : \@convert) );
}

sub _convertAnySloppy(@)
{   my ($self, $type, $nodes, $path, $read) = @_;

    my $key     = $read->keyRewrite($type);
    my $reader  = try { $self->reader($type) };
    if($@)
    {   # unknown type or untyped...
        my @convert = map XMLin($_), @$nodes;
        return ($key => @convert==1 ? $convert[0] : \@convert);
    }
    else
    {   trace "auto-convert known 'any' $type";
        my @nodes   = ref $nodes eq 'ARRAY' ? @$nodes : $nodes;
        my @convert = map $reader->($_), @nodes;

        ($key => @convert==1 ? $convert[0] : \@convert);
    }
}

1;
