#=============================================================================
# Package: PPM::PPD
# Purpose: Exposes a simple, object-oriented interfaces to PPDs.
# Notes:
# Author:  Neil Watkiss
# Date:    Mon Sep 10 10:47:08 PDT 2001
#=============================================================================
package PPM::PPD;

use strict;
use XML::Parser;
use Data::Dumper;

$PPM::PPD::VERSION = '3.00';

sub new {
    my $this = shift;
    my $ppd  = shift;
    die "Error: PPM::PPD constructor called with undef ppd\n" .
    Dumper(caller(0))
      unless defined $ppd;
    my $class = ref($this) || $this;
    my $self = {};
    bless $self, $class;
    $self->init($ppd);
    return $self;
}

sub is_complete {
    my $o = shift;
    $o->{is_complete};
}

sub find_impl {
    my $o = shift;
    my $target = shift;

    # We must not 'use' this, because the ppminst code also uses PPM::PPD, and
    # it doesn't have PPM::Result available, because it never needs to find an
    # implementation -- it's already installed!
    require PPM::Result;
    for my $impl ($o->implementations) {
	my $match = 1;
	for my $field (keys %$impl) {
	    next if ref($field);
	    my $value = $target->config_get($field);
	    next unless $value->is_success;
	    $match &&= ($value->result eq $impl->{$field});
	}
	return PPM::Result::Ok($impl) if $match == 1;
    }
    PPM::Result::Error("no suitable implementation found for '"
		       . $o->name . "'.");
}

sub name {
    my $o = shift;
    my $r = $o->{parsed}{NAME};
    return defined $r ? $r : "";
}

sub title {
    my $o = shift;
    my $r = $o->{parsed}{TITLE};
    return defined $r ? $r : "";
}

sub version {
    my $o = shift;
    my $r = $o->{parsed}{VERSION};
    return defined $r ? $r : "";
}

sub version_printable {
    my $o = shift;
    my $v = $o->version;
    printify($v);
}

sub printify {
    my $v = shift;
    $v =~ s/(?:,0)*$//;
    $v .= '.0' unless ($v =~ /,/ or $v eq '');
    $v = "(any version)" if $v eq '';
    $v =~ tr/,/./;
    $v;
}

# This sub returns 1 if $ver is >= to $o->version. It returns 0 otherwise.
# Note: this is only used if the repository doesn't know how to compare
# version numbers. The PPM3Server knows how to do it, the others don't.
sub uptodate {
    my $o = shift;
    my $ver = shift;

    return 1 if $ver eq $o->version; # shortcut

    my @required = split ',', $o->version;
    my @proposed = split ',', $ver;

    for (my $i=0; $i<@required; $i++) {
	return 0 if $proposed[$i] < $required[$i];	# too old
	return 1 if $proposed[$i] > $required[$i];	# even newer
    }
    return 1; # They're equal
}

sub abstract {
    my $o = shift;
    my $r = $o->{parsed}{ABSTRACT};
    return defined $r ? $r : "";
}

sub author {
    my $o = shift;
    my $r = $o->{parsed}{AUTHOR};
    return defined $r ? $r : "";
}

sub implementations {
    my $o = shift;
    return @{$o->{parsed}{IMPLEMENTATION} || []};
}

sub ppd {
    my $o = shift;
    return $o->{ppd};
}

sub init {
    my $o = shift;
    my $ppd = shift;

    if ($ppd =~ /<SOFTPKG/) {
	$o->{ppd} = $ppd;
	$o->{source} = caller;
    }
    elsif ($ppd !~ m![\n]! && -f $ppd) {
	$o->loadfile($ppd);
	$o->{source} = $ppd;
    }
    else {
	die "PPM::PPD::init: not a PPD and not a file:\n$ppd";
    }

    $o->parse;
    $o->prepare;
    delete $o->{parsetree};
    $o->{is_complete} = 1;
}

sub loadfile {
    my $o = shift;
    my $file = shift;
    open FILE, $file		|| die "can't read $file: $!";
    $o->{ppd} = do { local $/; <FILE> };
    close FILE			|| die "can't close $file: $!";
}

sub parse {
    my $o = shift;
    my $parser = XML::Parser->new(Style => 'Tree');
    $o->{parsetree} = eval { $parser->parse($o->{ppd}) };
    die "error: can't parse " . $o->{ppd} . ": $@" if $@;
}

sub prepare {
    my $o = shift;
    my $tree = $o->{parsetree};
    $o->{parsed} = $o->_reparse($tree);
}

sub _reparse {
    my $o = shift;
    my $tree = shift;
    my $ref = {};

    my $i;
    for ($i=0; $i<@$tree; $i++) { last if $tree->[$i] eq 'SOFTPKG' }
    die "error: no SOFTPKG element in PPD from $o->{source}"
      if $i >= @$tree;
    $tree = $tree->[$i+1];

    my $parse_elem = sub {
	my $ref = shift;
	my $tree = shift;
	my $key = shift;
	my $req = shift;
	my $content = shift; $content = 2 unless defined $content;
	my $cref = shift;
	my $i;
	for ($i=0; $i<@$tree; $i++) { last if $tree->[$i] eq $key }
	die "error: missing $key element in PPD from $o->{source}"
	  if $req && $i >= @$tree;
	return if $i >= @$tree;
	$cref->($ref, $key, $content, $tree->[$i+1]) if $cref;
	$ref->{$key} = $tree->[$i+1][$content] unless $cref;
    };

    my $parse_attr = sub {
	my $ref = shift;
	my $tree = shift;
	my $key = shift;
	my $req = shift;
	my $keephash = shift;
	my $cref = shift;
	die "error: missing $key attribute in PPD from $o->{source}" 
	  if $req && not exists $tree->[0]{$key};
	return if $i >= @$tree;
	$cref->($ref, $key, $keephash, $tree->[0]{$key}) if $cref;
	$ref->{$key} = $keephash ? $tree->[0] : $tree->[0]{$key} unless $cref;
    };

    my $parse_impls = sub {
	my $ref = shift;
	my $tree = shift;
	my $cref = sub {
	    my ($ref, $key, $content, $tree) = @_;
	    $ref->{$key} = (values %{$tree->[$content]})[0];
	};
	$parse_elem->($ref, $tree, 'ARCHITECTURE', 0, 0, $cref);
	$parse_elem->($ref, $tree, 'CODEBASE', 0, 0, $cref);
	$parse_elem->($ref, $tree, 'OS', 0, 0, $cref);
	$parse_elem->($ref, $tree, 'OSVERSION', 0, 0, $cref);
	$parse_elem->($ref, $tree, 'PERLCORE', 0, 0, $cref);
	$parse_elem->($ref, $tree, 'PYTHONCORE', 0, 0, $cref);

	# Now parse the DEPENDENCY section:
	for (my $i=0; $i<@$tree; $i++) {
	    next unless $tree->[$i] eq 'DEPENDENCY';
	    my $r = bless {}, 'PPM::PPD::Dependency';
	    $parse_attr->($r, $tree->[$i+1], 'NAME', 1);
	    $parse_attr->($r, $tree->[$i+1], 'VERSION', 0);
	    push @{$ref->{DEPENDENCY}}, $r;
	}
    };

    # First, get the NAME and VERSION tags
    $parse_attr->($ref, $tree, 'NAME', 1);
    $parse_attr->($ref, $tree, 'VERSION', 1);

    # Now validate the AUTHOR, ABSTRACT, and TITLE elements
    $parse_elem->($ref, $tree, 'AUTHOR', 0);
    $parse_elem->($ref, $tree, 'ABSTRACT', 0);
    $parse_elem->($ref, $tree, 'TITLE', 1);
    $ref->{ABSTRACT}	||= "(abstract)";
    $ref->{AUTHOR}	||= "(author)";

    # Now validate the IMPLEMENTATION sections.
    for (my $j=0; $j<@{$tree}; $j++) {
	next unless $tree->[$j] eq 'IMPLEMENTATION';
	my $r = bless {}, 'PPM::PPD::Implementation';
	$parse_impls->($r, $tree->[$j+1]);
	push @{$ref->{IMPLEMENTATION}}, $r;
    }

    $ref;
}

package PPM::PPD::Dependency;

sub name {
    my $o = shift;
    my $r = $o->{NAME};
    return defined $r ? $r : "";
}

sub version {
    my $o = shift;
    my $r = $o->{VERSION};
    return defined $r ? $r : "";
}

sub version_printable {
    goto &PPM::PPD::version_printable;
}

sub uptodate {
    goto &PPM::PPD::uptodate;
}

package PPM::PPD::Implementation;

sub codebase {
    my $o = shift;
    my $r = $o->{CODEBASE};
    return defined $r ? $r : "";
}

sub os {
    my $o = shift;
    my $r = $o->{OS};
    return defined $r ? $r : "";
}

sub osversion {
    my $o = shift;
    my $r = $o->{OSVERSION};
    return defined $r ? $r : "";
}

sub osversion_printable {
    my $o = shift;
    my $r = $o->osversion;
    PPM::PPD::printify($r);
}

sub architecture {
    my $o = shift;
    my $r = $o->{ARCHITECTURE};
    return defined $r ? $r : "";
}

sub pythoncore {
    my $o = shift;
    my $r = $o->{PYTHONCORE};
    return defined $r ? $r : "";
}

sub perlcore {
    my $o = shift;
    my $r = $o->{PERLCORE};
    return defined $r ? $r : "";
}

sub prereqs {
    my $o = shift;
    return @{$o->{DEPENDENCY} || []};
}

package PPM::PPD::Search;
@PPM::PPD::Search::ISA = 'PPM::Search';

use Data::Dumper;

sub matchimpl {
    my $self = shift;
    my ($impl, $field, $re) = @_;
    if ($field eq 'OS')			{ return $impl->os =~ $re }
    elsif ($field eq 'OSVERSION')	{ return $impl->osversion =~ $re }
    elsif ($field eq 'ARCHITECTURE')	{ return $impl->architecture =~ $re}
    elsif ($field eq 'CODEBASE')	{ return $impl->codebase =~ $re }
    elsif ($field eq 'PYTHONCORE')	{ return $impl->pythoncore =~ $re }
    elsif ($field eq 'PERLCORE')	{ return $impl->perlcore =~ $re }
    else {
	warn "unknown search field '$field'" if $^W;
    }
}

sub match {
    my $self = shift;
    my ($ppd, $field, $match) = @_;
    my $re = qr/$match/;
    $field = uc($field);
    if ($field eq 'NAME')	 { return $ppd->name =~ $re }
    if ($field eq 'AUTHOR')      { return $ppd->author =~ $re }
    if ($field eq 'ABSTRACT')    { return $ppd->abstract =~ $re }
    if ($field eq 'TITLE')       { return $ppd->title =~ $re }
    if ($field eq 'VERSION')     { return $ppd->version_printable =~ $re }
    return (grep { $_ }
	    map { $self->matchimpl($_, $field, $re) }
	    $ppd->implementations);
}

unless (caller) {
    my $dat = do { local $/; <DATA> };
    eval $dat;
    die $@ if $@;
}

1;
__DATA__

package main;
use Data::Dumper;

my $ppd = PPM::PPD->new("./Tk-JPEG.ppd");

print Dumper $ppd;
print Dumper [$ppd->name,
	      $ppd->version,
#	      $ppd->title,
	      $ppd->abstract,
	      $ppd->author,
	      $ppd->implementations(),
	     ];

