# $Revision: 1.16 $
# $Id: Entry.pm,v 1.16 2002/07/22 08:35:43 afoxson Exp $

# Mail::Freshmeat::Entry - parses entries from freshmeat daily newsletters
# Copyright (c) 2002 Adam J. Foxson. All rights reserved.

# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

package Mail::Freshmeat::Entry;

use strict;
use 5.005;
use Carp;
use vars qw($VERSION $AUTOLOAD);
use Mail::Freshmeat::Utils;

local $^W;

($VERSION) = '$Revision: 1.16 $' =~ /\s+(\d+\.\d+)\s+/;

sub new
{
	my $type  = shift;
	my $entry = shift or croak "I need to be passed an entry.";
	my $count = shift or croak "I need to be passed a count.";
	my $class = ref($type) || $type;
	my $self  = bless {}, $class;

	$self->{_attrs} =
	[
		qw
		(
			_position _name_and_version _name _version _posted_by_name
			_posted_by_url _posted_on _trove _about _changes _license _url
		)
	];

	# these are the allowed entry accessors
	$self->{_is_attr} = {map {$_ => 1} @{$self->{_attrs}}, '_full'};
	$self->_parse($entry, $count);

	return $self;
}

sub entry_keys
{
	my $self = shift;
	wantarray ? @{$self->{_attrs}} : $self->{_attrs};
}

sub _parse
{
	my $self  = shift;
	my $entry = shift or croak "I need to be passed an entry.";
	my $count = shift or croak "I need to be passed a count.";
	my @entries;

	if ($entry =~
	/
		^ \s* \[(\d+)\] \s-\s (.*) $ \n
		^ (\s* .* \)) \s* by\s(.*) \s \((.*)\) $ \n
		^ \s* ( \w+ , \s \w+ \s \d{1,2} \w{2} \s \d{4} \s \d{2}:\d{2} ) $ \n

		(?: 
			$_blank_line
			(?s: (?:Category:\s|Categories:\s)? (.+?) \n )
		)?
		$_blank_line
		(?s: About:\s (.+?) \n )
		(?: 
			$_blank_line
			(?s: Changes:\s (.+?) \n )
		)?
		$_blank_line
		^ \s* License:\s (.*) $ \n
		$_blank_line
		^ \s* URL:\s (.*) $
	/mx)
	{
		$self->{_position}         = $1;
		$self->{_name_and_version} = $2 . $3;
		$self->{_posted_by_name}   = $4;
		$self->{_posted_by_url}    = $5;
		$self->{_posted_on}        = $6;
		$self->{_trove}            = $7;
		$self->{_about}            = $8;
		$self->{_changes}          = $9;
		$self->{_license}          = $10;
		$self->{_url}              = $11;
		$self->{_full}             = $entry;
	}
	elsif ($entry =~
	/
		^ \s* \[(\d+)\] \s-\s (.*) $ \n
		^ \s* by\s(.*) \s \((.*)\) $ \n
		^ \s* ( \w+ , \s \w+ \s \d{1,2} \w{2} \s \d{4} \s \d{2}:\d{2} ) $ \n

		(?: 
			$_blank_line
			(?s: (?:Category:\s|Categories:\s)? (.+?) \n )
		)?
		$_blank_line
		(?s: About:\s (.+?) \n )
		(?: 
			$_blank_line
			(?s: Changes:\s (.+?) \n )
		)?
		$_blank_line
		^ \s* License:\s (.*) $ \n
		$_blank_line
		^ \s* URL:\s (.*) $
	/mx)
	{
		$self->{_position}         = $1;
		$self->{_name_and_version} = $2;
		$self->{_posted_by_name}   = $3;
		$self->{_posted_by_url}    = $4;
		$self->{_posted_on}        = $5;
		$self->{_trove}            = $6;
		$self->{_about}            = $7;
		$self->{_changes}          = $8;
		$self->{_license}          = $9;
		$self->{_url}              = $10;
		$self->{_full}             = $entry;
	}
	elsif ($entry =~
	/
		^ \s* \[(\d+)\] \s-\s (.*) $ \n
		^ \s* by\s(.*) $ \n
		^ (\s* .*) $ \n
		^ \s* ( \w+ , \s \w+ \s \d{1,2} \w{2} \s \d{4} \s \d{2}:\d{2} ) $ \n

		(?: 
			$_blank_line
			(?s: (?:Category:\s|Categories:\s)? (.+?) \n )
		)?
		$_blank_line
		(?s: About:\s (.+?) \n )
		(?: 
			$_blank_line
			(?s: Changes:\s (.+?) \n )
		)?
		$_blank_line
		^ \s* License:\s (.*) $ \n
		$_blank_line
		^ \s* URL:\s (.*) $
	/mx)
	{
		$self->{_position}         = $1;
		$self->{_name_and_version} = $2;
		$self->{_posted_by_name}   = $3 . $4;
		$self->{_posted_on}        = $5;
		$self->{_trove}            = $6;
		$self->{_about}            = $7;
		$self->{_changes}          = $8;
		$self->{_license}          = $9;
		$self->{_url}              = $10;
		$self->{_full}             = $entry;

		($self->{_posted_by_name}, $self->{_posted_by_url}) =
			$self->{_posted_by_name} =~ /(.*) \s \((.*)\)/;
	}
	else
	{
		_fatal_bug("Couldn't parse entry $count (entries).");
	}

	@$self{qw/_name _version/} =
		$self->_parse_entry_version($self);

	for my $key (keys %$self)
	{
		$self->{$key} = '' if not defined $self->{$key};
	}

	if ($self->position() != $count)
	{   
		_fatal_bug("Detcted an entry with an incorrect position " .
			"(${\($self->position())}/$count).");
	}

	return $self;
}

# TODO: One day this will probably be have to be re-written. As it is now
# it parses the very vast majority name-version's successfully, but I'd
# like to get it to 100%
sub _parse_entry_version
{
	my $self = shift;

	# Start of first word of version must match this
	my $version_first_word_start = qr
	/   
		(   
			[.\d]           |
			pre             |
			alpha           |
			beta            |
			patch           |
			r               |
			rel             |
			release         |
			build           |
			v(?:er)? [^a-z]
		)
	/ix;

	# Start of further words of version must match this
	my $version_other_words_start = qr
	/   
		(   
			[.\d(]          |
			pre             |
			alpha           |
			beta            |
			r               |
			rel             |
			release         |
			build           |
			patch
		)
	/ix;

	# Rest of each word of version must match this
	my $version_rest_of_word = qr
	/   
		(   
			[.\w()\/-]      |
			pre             |
			alpha           |
			beta            |
			patch           |
			\d{1,6}(?!\d)       # not more than six digits
								# in a row
		)*
	/ix;

	my ($name, $version) = ($self->{_name_and_version}, '');

	if ($self->{_name_and_version} =~
	/^  
		(.+?)
		\s+
		(
			$version_first_word_start
			$version_rest_of_word
			(?: 
				\s+
				$version_other_words_start
				$version_rest_of_word
			)*
		)
	$/ix)
	{   
		$name    = $1;
		$version = $2;
	}

	return ($name, $version);
}

sub short_entry
{
	my $self = shift;
	return $self->position(), " - ", $self->name_and_version();
}   

sub AUTOLOAD
{
	my $self = $_[0];
	my ($package, $method) = ($AUTOLOAD =~ /(.*)::(.*)/);

	return if $method =~ /^DESTROY$/;
	unless ($self->{_is_attr}->{"_$method"})
	{
		croak "No such entry accessor entry: $method; aborting";
	}

	my $code = q
	{
		sub
		{   
			my $self = shift;
			return $self->{_METHOD};
		}
	};

	$code =~ s/METHOD/$method/g;

	{
		no strict 'refs';
		*$AUTOLOAD = eval $code;
	}

	goto &$AUTOLOAD;
}

1;


syntax highlighted by Code2HTML, v. 0.9.1