package JIB::Installation;

use strict;
use warnings;

use JIB::Constants;
use JIB::Package;
use JIB::Config;
use JIB::Utils;
use JIB::Meta;
use JIB::Alternative;

use YAML                    qw[LoadFile DumpFile];
use Params::Check           qw[check];
use Log::Message::Simple    qw[:STD];
use File::Basename          qw[basename];
use Path::Class             ();
use Data::Dumper;

use base 'Object::Accessor';

=head1 ACCESSORS

=head2 $dir = $inst->meta_dir

=cut

sub meta_dir {
    my $self = shift;
    my $dir  = $self->dir or return;

    return $dir->subdir( $self->config->_meta_dir);
}

=head2 $dir = $inst->control_dir( $pkg_name )

=cut

sub control_dir {
    my $self = shift;
    my $pkg  = shift;       return unless defined $pkg;
    my $dir  = $self->dir   or return;

    return $dir->subdir( $self->config->_control )->subdir( $pkg );
}


=head2 $dir = $inst->alternatives_dir

=cut

sub alternatives_dir {
    my $self = shift;
    my $dir  = $self->dir or return;

    return $dir->subdir( $self->config->_alternatives );
}

=head2 $file = $inst->registered_alternatives_file

=cut

sub registered_alternatives_file {
    my $self = shift;
    my $dir  = $self->dir or return;

    return $dir->file( $self->config->_registered_alternatives );
}

=head2 $file = $inst->available_file

=cut

sub available_file {
    my $self = shift;
    my $dir  = $self->dir or return;

    return $dir->file( $self->config->_available );
}

=head2 $file = $inst->files_list( $pkg_name )

=cut

sub files_list {
    my $self = shift;
    my $pkg  = shift        or return;
    my $dir  = $self->dir   or return;

    return $self->control_dir( $pkg )
                ->file( $self->config->_files_list );
}

=head1 METHODS

=head2 $inst = JIB::Installation->new;

=cut

### XXX do some smart caching here -- but can't just make it a singleton
### due to stacked installations
{   my $config  = JIB::Config->new;

    my %cache = ();

    sub new { 
        my $class   = shift;
        my %hash    = @_;
    
        my $dir;
        my $tmpl    = {
            dir => { required => 1, allow => DIR_EXISTS, store => \$dir },
        };
    
        check( $tmpl, \%hash ) or error( Params::Check->last_error ), return;
    
        ### make the path absolute
        ### XXX path::class doesn't seem to have a rel2abs
        $dir = Path::Class::dir( File::Spec->rel2abs( $dir ) );

        ### singleton
        return $cache{$dir} if $cache{$dir};
    
        my $obj     = $class->SUPER::new;
    
        ### XXX tidy up
        {   $obj->mk_accessors( qw[available registered_alternatives config 
                                   dir] 
                            );
            
            ### set these 2 first, the other methods rely on it
            $obj->dir( $dir );
            $obj->config( $config );
            
            ### if the metadir doesn't exist yet, this is a 'clean' dir,
            ### and we need to set it up for holding meta-data
            unless ( -d $obj->meta_dir ) {
                
                ### set up the dirs
                for my $subdir ($obj->meta_dir, $obj->alternatives_dir,
                                $obj->control_dir(''), 
                ) {
                    system( qw[mkdir -p], $subdir )     and error($?), return;
                }
        
                ### set up the files
                for my $meth (qw[registered_alternatives_file available_file]) {
                    system( qw[touch], $obj->$meth )    and error($?), return;
                }
            }
            
            ### do alts first, package;:installed uses them
            my @alts = eval { 
                map { JIB::Alternative->new_from_struct( struct => $_ ) }
                    LoadFile( $obj->registered_alternatives_file ) 
            };
            $@ and error( "Could not load registered alts file: $@" ), return;

            $obj->registered_alternatives( \@alts );

            my @avail = eval { 
                map { JIB::Package->new( meta => $_, installation => $obj) }
                map { JIB::Meta->new_from_struct( struct => $_ ) }
                    LoadFile( $obj->available_file ) 
            };
            $@ and error( "Could not load available file: $@" ), return;
            $obj->available( \@avail );

        }
  
        return $obj;
    }
}

=head2 $inst_pkg = $inst->is_installed( package => $package )

=cut

### XXX package name?
sub is_installed {
    my $self = shift;
    my %hash = @_;
    
    my $pkg;
    my $tmpl = {
        package => { required => 1, store => \$pkg, allow => ISA_JIB_PACKAGE },
    };
    
    check( $tmpl, \%hash ) or error( Params::Check->last_error ), return;

    for my $inst ( @{ $self->available } ) {
        return $inst if $inst->package eq $pkg->package;
    }
    
    return;
}    

=head2 $inst_pkg = $inst->register( package => $package )

=cut

sub register {
    my $self = shift;
    my $conf = $self->config;
    my %hash = @_;
    
    my $pkg;
    my $tmpl = {
        package => { required => 1, store => \$pkg, allow => ISA_JIB_PACKAGE },
    };
    
    check( $tmpl, \%hash ) or error( Params::Check->last_error ), return;

    my $inst_pkg = JIB::Package->new(meta => $pkg->meta, installation => $self)
        or error("Could not create installed package"), return;
 
    LINKING: { 
        ### XXX config!
        my $my_bindir = File::Spec->catdir( 
                            $self->dir, $pkg->package, 'bin' );

        last LINKING unless -d $my_bindir;

        ### load in the alternatives collection
        my @alts = @{ $self->registered_alternatives };
        
        ### check if we're the 'prefered' package
        my $link_this   = 1;
        my $unlink_this = '';
        
        for my $test ( @alts ) {
            ### XXX this should be a policy test!
            if( $pkg->prefix    eq $test->prefix    and
                $pkg->name      eq $test->name      and
                $pkg->version   <= $test->version
            ) {
                $link_this      = 0;  
                $unlink_this    = $test;
                last;
            }
            
            ### XXX clean up links from $unlink_this
        }      

        last LINKING unless $link_this;

        ### remove the registered alternative
        $self->remove_registered_alternative( package => $unlink_this )
            if $unlink_this;

        ### link the files
        return unless $self->link_files( package => $inst_pkg );

    }

    ### update the available files
    $self->available( [ @{ $self->available }, $inst_pkg ] );
        
    $self->write or return;
    
    return $inst_pkg;

}

=head2 $bool = $inst->unregister( package => $inst_pkg );

=cut

sub unregister {
    my $self = shift;
    my $conf = $self->config;
    my %hash = @_;
    
    
    my $inst_pkg;
    my $tmpl = {
        package => { required => 1, store => \$inst_pkg, 
                        allow => ISA_JIB_PACKAGE_INSTALLED },
    };
    
    check( $tmpl, \%hash ) or error( Params::Check->last_error ), return;

=pod
    
    my @list        = LoadFile( $Available ); 
    my @uninstalled = grep { $_->{package} ne $pkg } @list;
    
    ### check if we're even allowed to delete this, due to depends
    {   my $info = LoadFile( $meta .'/'. $Metafile );

        my $delete_ok = 1;

        for my $entry ( @list ) {
            for my $depends ( list_dependencies( $entry ) ) {
                
                ### check if this entry depends on /any/ of the items
                ### we provide
                if( dependency_satisfied_by( $depends, $info ) ) {

                    ### if the dependency is also sastisfied by /another/
                    ### package, it's still safe to delete us, otherwise not
                    if( !dependency_satisfied( $depends, \@uninstalled ) ) {

                        warn "\t*** $entry->{package} depends on $pkg ***\n";
                        $delete_ok = 0;
                    }
                }
            }      
        }
        
        die "Not allowed to delete '$pkg'\n" unless $delete_ok;
    }
=cut
    
    ### uninstall the files
    ### XXX check dependencies
    {   open my $fh, $self->files_list( $inst_pkg->package )        or die $!;
    
        my $prerm = $self->control_dir( $inst_pkg->package )
                         ->file( $conf->prerm );
        if( -e $prerm && -s _ ) {
            system( qq[ $^X $prerm ] )                              and die $?;
        }
        
        while( <$fh> ) {
            chomp;
            msg("Removing file '$_'", 1);
            -e $_ && system(qq[rm -rf $_])                          and die $?;
            
            die "File '$_' not removed" if -e $_;
        }
        close $fh;
    
        ### XXX need status dir like dpkg
        my $postrm = $self->control_dir( $inst_pkg->package )
                          ->file( $conf->postrm );
        if( -e $postrm && -s _ ) {
            system( qq[ $^X $postrm ] )                             and die $?;
        }
    }


    ### remove alternatives and relink if needed
    ### XXX doesn't do manpages yet
    ### XXX doens't check the AUTO flag yet for link management
    LINKING: {   
        my $alt = $self->remove_registered_alternative( package => $inst_pkg );

        ### this package didn't provide any alternatives
        last LINKING unless $alt;
        
        ### XXX this should probably be done in one go, so we don't
        ### have a situation where no 'script' is available
        
        ### unlink all the script files
        for my $script ( @{ $alt->bin || [] } ) {
            1 while unlink $conf->bin_dir->file( $script );
            1 while unlink $self->alternatives_dir->file( $script ); 
        }      
        
        ### see if there's any other package that's now the default
        ### for this module
        ### make sure we dont see ourselves again, so grep that out
        my $new_alt;
        {   my $wanted = join '-',  $inst_pkg->prefix, $inst_pkg->name;

            ### find all packages that provide: a <prefix>-<name>
            ### implementation;
            my @list = @{ $self->available };
            
            my @maybe;
            for my $test ( grep { $_->package ne $inst_pkg->package } @list ) {
                push @maybe, $test if grep {
                        $_ eq $wanted
                    } @{ $test->provides || [] };
            }
        
            ### find the alternative with the highest version
            ### XXX this should be policy based!
            ($new_alt) = sort { $b->version <=> $a->version } @maybe;
        }

        ### no alt? bail!
        last LINKING unless $new_alt;
    
        ### link the files
        return unless $self->link_files( package => $new_alt );
    }

    ### remove this package from the available list
    $self->remove_available( package => $inst_pkg );
    
    ### move this all to Installation.pm
    $self->write or return;
    
    ### unisntall metadata
    system( qq[rm -rf ] . $self->control_dir( $inst_pkg->package ) ) and die $?;

    msg( "Package '".$inst_pkg->package."' and associated metadata removed");

    return 1;
}

=head2 $alt = $inst->remove_registered_alternative( package => $inst_pkg )

=cut

sub remove_registered_alternative {
    my $self = shift;
    my %hash = @_;
    
    my $inst_pkg;
    my $tmpl = {
        package => { required => 1, store => \$inst_pkg, 
                        allow => ISA_JIB_PACKAGE_INSTALLED },
    };
    
    check( $tmpl, \%hash ) or error( Params::Check->last_error ), return;

    my $alt; 
    my @alts = map  { $_->[0] } # return the object
               grep { $_->[1] } # flag toggled?
               map  { $_->package eq $inst_pkg->package
                        ? do { $alt = $_; [ $_, 0 ] }
                        : [ $_, 1 ]
                } @{ $self->registered_alternatives }; 

    $self->registered_alternatives( \@alts ) if $alt;

    return $alt if $alt;
    return;
}    

=head2 $bool = $inst->remove_available( package => $inst_pkg );

=cut

sub remove_available {
    my $self = shift;
    my %hash = @_;
    
    my $inst_pkg;
    my $tmpl = {
        package => { required => 1, store => \$inst_pkg, 
                        allow => ISA_JIB_PACKAGE_INSTALLED },
    };
    
    check( $tmpl, \%hash ) or error( Params::Check->last_error ), return;

    ### remove this package from the available list
    ### XXX temp file, then mv
    my @prev  = @{ $self->available };
    my @avail = grep { $_->package ne $inst_pkg->package } @prev;
    
    ### no change!
    return if @avail == @prev;
    
    $self->available( \@avail );
    return 1;
}

=head2 $bool|$alt = $inst->link_files( package => $inst_pkg );

=cut

### XXX no manpages yet
sub link_files {
    my $self = shift;
    my $conf = $self->config;
    my %hash = @_;
    
    my $inst_pkg;
    my $tmpl = {
        package => { required => 1, store => \$inst_pkg, 
                        allow => ISA_JIB_PACKAGE_INSTALLED },
    };
    
    check( $tmpl, \%hash ) or error( Params::Check->last_error ), return;

    my $my_bindir =  $self->dir->subdir( $inst_pkg->package )->subdir('bin');

    ### XXX doesn't return an object...
    return 1 unless -d $my_bindir;

    my @bins;
    msg( "Linking scripts/manpages to " . $inst_pkg->package, 1 );
    for ( qx[find $my_bindir -type f] ) {
        chomp;
        
        ### link from altdir to install dir
        ### then from pathdir, to altdir
        my $script = basename($_);
    
        my $alt = $self->alternatives_dir->file( $script );
        my $bin = $conf->bin_dir->file( $script );
        system( qq[ln -fs $_ $alt] )                    and die $?;
        system( qq[ln -fs $alt $bin ] )                 and die $?;
        push @bins, $script;
    }

    my $alt = JIB::Alternative->new_from_struct( struct => 
                    { bin => \@bins, auto => 1, package => $inst_pkg->package } 
                ) or return;

    ### dump out alternatives again
    ### XXX pretty method?
    $self->registered_alternatives([ @{$self->registered_alternatives}, $alt ]);

    return $alt;
}

=head2 $bool = $inst->write;

=cut

sub write {
    my $self = shift;
    my $conf = $self->config;
    
    ### XXX use tempfiles

    my @avail = map { $_->meta->to_struct } @{ $self->available };
    eval { DumpFile( $self->available_file, @avail ) };
    $@ and error( "Could not write available file: $@" ), return;
    
    my @alts = map { $_->to_struct } @{ $self->registered_alternatives };
    eval { DumpFile( $self->registered_alternatives_file, @alts ) };
    $@ and error( "Could not write alternatives file: $@" ), return;

    return 1;
}

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

1;

















