package JIB::Dependency;

use strict;
use warnings;

use Params::Check   qw[check];

use JIB::Constants;
use JIB::Config;

use base 'Object::Accessor';

=head2 $obj = JIB::Dependency->new_from_struct( struct => STRUCT )

=cut

sub new_from_struct {
    my $class = shift;
    my %hash  = @_;
    
    my $struct;
    my $tmpl = {
        struct => { required => 1, store => \$struct },
    };
    
    check( $tmpl, \%hash ) or error( Params::Check->last_error ), return;

    my $obj = $class->SUPER::new;
    $obj->mk_accessors( keys %$tmpl );
    
    $obj->struct( $struct );

    return $obj;
}

=head2 $str = $self->pretty_print

Pretty prints the dependency declaration from the META.info.
It will turn an entry like this:

depends:
  - p5-a
  -
    - p5-b: 3
    - p5-b: < 2
  - !perl/ref:
    =:
      - p5-c
      -
        - p5-e
        - p5-d

Into:

 p5-a AND (p5-b >= 3 OR p5-b < 2) AND p5-c AND (p5-e OR p5-d)

=cut

### XXX combine with the dependency checker?
sub pretty_print {
    my $self    = shift;
    my $struct  = $self->struct;
    
    return $self->_pp_from_struct( $struct );
}

sub _pp_from_struct {
    my $self    = shift;
    my $struct  = shift;
    my $str     = '';
    
    return $str unless $struct;

    for my $entry ( @$struct ) {
        $str .= ' AND ';

        if( ! ref $entry ) {
            $str .= "$entry";
    
        } elsif ( UNIVERSAL::isa( $entry, 'ARRAY' ) ) {
            $str .= '(';
            $str .= join " OR ", map { $self->_pp_from_struct( [$_] ) } @$entry;
            $str .= ')';
            
        } elsif ( UNIVERSAL::isa( $entry, 'REF' ) ) {
            $str .= $self->_pp_from_struct( $$entry );
        } elsif ( UNIVERSAL::isa( $entry, 'HASH' ) ) {
            while (my($k,$v) = each %$entry) {
                my ($ver,$op) = reverse split /\s+/, $v;
                $op ||= '>=';
                $str .= "$k $op $ver";
            }
        } else {
            error("Illegal token: $entry\n"), return;
        }
    }
    
    $str =~ s/^ AND //;
    
    return $str;
}    

sub list_missing_dependencies {
    my $self = shift;
    my %hash = @_;
    
    my($repo_set, $inst_set);
    my $tmpl = {
        repository_set => { 
            required    => 1, 
            store       => \$repo_set,
            allow       => ISA_JIB_REPOSITORY_SET,
        },
        installation_set => { 
            required    => 1, 
            store       => \$inst_set,
            allow       => ISA_JIB_INSTALLATION_SET, 
        },
    };                             

    check( $tmpl, \%hash ) or error( Params::Check->last_error ), return;

    


}

1;

__END__

    ### takes the metadata object of 1 package, and returns a list of
    ### its dependencies
    sub list_dependencies {
        my $meta    = shift or die "Meta object required";
        my $print   = shift || 0;   # print the parsed dependencies?
        my @deps    = $meta->{depends} ? @{ $meta->{depends} } : ();
        my @index   = LoadFile( $protoconf::Repoindex ); 
        my @avail   = LoadFile( $protoconf::Available );

        ### XXX move the AND to the subroutine
        if( $print and @deps ) {
            print "Translating (dependency for $meta->{package}):\n";
            print Dump( $meta->{depends} );
            #print "\n\nTo:\n";
            #print Dumper( $meta->{depends} );
            print "\n\nTo:\n";
            print _pp_depends( \@deps );
            print $/ . $/;
        }
        
        my @resolved = _parse_depends( \@deps, \@index, \@avail );

    }

    ### XXX merge with _pp_depends!      
    sub _parse_depends {
        my $deps    = shift;
        my $index   = shift or return;
        my $avail   = shift or return;

        my @maybe;
        ### AND context
        RESOLVE: for my $entry ( @$deps ) {
        
            ### plain old entry, we want to resolve this one
            if( ! ref $entry ) {
                
                ### do we have something that satisfies this dep already?
                for my $maybe (@$avail) {
                    next RESOLVE if dependency_satisfied_by($entry, $maybe);
                }

                my @found;
                ### find every package that satisfies this dependency
                for my $maybe (@$index) {
                    ### not the right package
                    next unless dependency_satisfied_by( $entry, $maybe );               

                    push @found, $maybe;
                }
               
                ### nothing satisfied this depenendency
                die "No package satisfies '$entry'\n" unless @found;
                
                ### XXX should be policy based
                my @sorted = sort { $b->{version} <=> $a->{version } } @found;

                push @maybe, $sorted[0];
                
        
            ### OR dependency
            } elsif ( UNIVERSAL::isa( $entry, 'ARRAY' ) ) {

                ### do we have something that satisfies this dep already?
                for my $sub_entry ( @$entry ) {
                    ### $avail, [] is NOT a typo -- rather than having
                    ### _parse_depends see if there's a suitable candidate
                    ### in the whole index, we only let it look at the 
                    ### installed packages. This way, we're sure that when
                    ### it returns a match, it found it in what we already
                    ### had, therefor we don't need to install it
                    ### This above section works for an OR search.
                    ### The [ ] is passed for the AND search, which will
                    ### then not be able to short circuit, as there are
                    ### no available modules on that check
                    eval { _parse_depends( [$sub_entry], $avail, [] ) }
                        and next RESOLVE;

                    
                }


                my @found;
                for my $sub_entry ( @$entry ) {

                    ### we found one that satisfied
                    if( @found = 
                        eval { _parse_depends( [$sub_entry], $index, $avail ) } 
                    ) {

                        ### add to our list and short circuit
                        push @maybe, $found[0];
                        last;
                        
                    ### OR dependency, so try the next                        
                    } elsif ( $@ =~ /^No package satisfies/ ) {
                        next;

                    ### some other error
                    } else {
                        die $@;
                    }                            
                }
    
            ### grouping    
            } elsif ( UNIVERSAL::isa( $entry, 'REF' ) ) {

                ### a mere context switch, just return what ever
                ### the recursive call returns
                push @maybe, _parse_depends( $$entry, $index, $avail );
    
            ### specific version
            } elsif ( UNIVERSAL::isa( $entry, 'HASH' ) ) {
                while (my($k,$v) = each %$entry ) {
                    my ($ver,$op) = reverse split /\s+/, $v;
                    $op ||= '>=';

                    ### do we have something that satisfies this dep already?
                    for my $maybe (@$avail) {
                        next unless dependency_satisfied_by($entry, $maybe);
                        
                        ### is the version good enough?
                        if ( eval "$maybe->{version} $op $ver" ) {
                            keys %$entry;   # reset keys!!!!!
                            next RESOLVE;
                        }
                    }

                    my @found;
                    ### find every package that satisfies this dependency
                    for my $maybe (@$index) {
                        ### not the right package
                        next unless dependency_satisfied_by( $k, $maybe );
    
                        ### not a good enough version
                        next unless eval "$maybe->{version} $op $ver";
                      
                        push @found, $maybe;
                    }

                    ### nothing satisfied this depenendency
                    unless( @found ) {
                        keys %$entry;   # reset keys!!!!!
                        die "No package satisfies '$k'\n" 
                    }

                    ### XXX should be policy based
                    my @sorted = 
                            sort { $b->{version} <=> $a->{version } } @found;

                    ### store the best match
                    push @maybe, $sorted[0];
                }
    
            } else {
                die "Illegal token: $deps\n";
            }
        }
        return @maybe;
    }
        
     
    ### takes a string with a wanted package and an optional available list
    ### and check if the package is available/provided by any installed pkg
    sub dependency_satisfied {
        my $wanted  = shift or die "Wanted package required";
        my @list    = @{ shift || [] };
        @list       = LoadFile( $protoconf::Available ) unless @list;
    
        ### XXX objectify properly
        ### XXX no | dependencies, no versions
        ### defaults to not satisfied, if we got dependencies
        my $satisfied = 0;
       
        ++$satisfied if grep {
                            $wanted eq $_
                        } map { 
                            ref $_->{provides} 
                                ? @ { $_->{provides} } 
                                : $_->{provides}
                                ;
                        } @list;
                        
        return $satisfied;
    }  

    ### takes a wanted string and a meta object, and checks if the meta object
    ### provides the wanted string
    sub dependency_satisfied_by {
        my($wanted, $meta) = @_ or die "Wanted & Meta required";
        
        return scalar grep { 
                        $wanted eq $_ 
                    } ref $meta->{provides} 
                        ? @ { $meta->{provides} } 
                        : $meta->{provides} || ();
    }      
}


# Local variables:
# c-indentation-style: bsd
# c-basic-offset: 4
# indent-tabs-mode: nil
# End:
# vim: expandtab shiftwidth=4:
