package URI::Collection;

use strict;
use vars qw($VERSION);
$VERSION = '0.01.1';

use Cwd;
use File::Find;
use File::Path;
use Config::IniFiles;
use Netscape::Bookmarks;

# Declare our internal category/link structure.
my %cat_links;

# PUBLIC METHODS

# XXX The fact that this is created as an object is just a thin veil
# XXX of appearance, so that methods can be called.  A procedural
# XXX interface would work, but just wouldn't be as shiny, IMHO.
sub new {
    my ($class, %args) = @_;
    my $self = {};

    _traverse ($args{directory}) if exists $args{directory};
    _parse_file ($args{file}) if exists $args{file};

    bless $self, $class;
    return $self;
}

sub as_bookmark_file {
    my ($self, %args) = @_;

    my $top = Netscape::Bookmarks::Category->new ({
        folded => 0,
        title => __PACKAGE__ . ' Bookmarks',
        add_date => time (),
        description => 'Bookmarks generated by ' . __PACKAGE__,
    });

    my %categories;

    for my $path (sort keys %cat_links) {
        my $category;

        for my $title (split /\//, $path) {
            $category = $top unless $category;

            unless (exists $categories{$title}) {
                $categories{$title} = Netscape::Bookmarks::Category->new ({
                    folded => 0,
                    title => $title,
                    add_date => time (),
                    description => '',
                });

                $category->add ($categories{$title});
            }

            $category = $categories{$title};
        }

        for my $link (@{ $cat_links{$path} }) {
            if (ref $link->{obj} eq 'Netscape::Bookmarks::Link') {
                # Handle a Netscape style entry.
                $category->add ($link->{obj});
            }
            else {
                # Handle a Windows Favorite entry.
                $category->add (favorite_to_bookmark ($link));
            }
        }
    }

    return $top->as_string ();
}

sub as_favorite_directory {
    my ($self, %args) = @_;

    # Create a top level directory for our Favorites.
    # NOTE: If we don't create an extra level here, we will 
    # over-write our Favorites.
    # XXX This can't be the right way to do it.
    my $top = 'Favorites-' . time ();
    mkdir $top;
    chdir $top;
    $top = getcwd;

    # Build the Favorites tree with Internet Shortcut files.
    for my $path (keys %cat_links) {
#        print "[$path]\n";
        mkpath $path;
        chdir $path;

        for my $link (@{ $cat_links{$path} }) {
            if (ref $link->{obj} eq 'Config::IniFiles') {
                # Handle a Windows Favorite entry.
                $link->{obj}->WriteConfig ("$link->{title}.url");
            }
            else {
                # Handle a Netscape style entry.
                my ($title, $obj) = bookmark_to_favorite ($link);
                # XXX Sanitize the title as a proper filename.
                $title =~ s/[^\w\s$%\-@~`'!()^#&+,;=.\[\]{}]/_/g;
                $obj->WriteConfig ("$title.url");
            }
        }

        chdir $top;
    }

    return $top;
}

# PRIVATE FUNCTIONS

sub _traverse { find(\&_wanted, @_) }
sub _wanted {
    if (/^(.+?)\.url$/) {
        my $title = $1;
#        print "\t$title\n";
        push @{ $cat_links{$File::Find::dir} }, {
            title => $title,
            obj => Config::IniFiles->new (-file => "$title.url"),
        };
    }
}

sub _parse_file {
    my $b = Netscape::Bookmarks->new (shift);

    my (@category, $category);
    my $last_level = 0;

    my $sub = sub {
        my ($object, $level) = @_;

        if ($object->isa ('Netscape::Bookmarks::Category')) {
            # Find the current / separated category name.
            if ($level > 0) {
                if ($level <= $last_level) {
                    # XXX splice () would be more idiomatic...
                    pop @category for 1 .. $last_level - $level + 1;
                }

                push @category, $object->title;
            }

            $category = join '/', @category;
            $last_level = $level;

#            print "[$category]\n";
#            print "\t" x $level .'['. $object->title ." ($level)]\n";
        }
        elsif ($object->isa ('Netscape::Bookmarks::Link')) {
#            print "\t" . $object->title . "\n";
            push @{ $cat_links{$category} }, {
                title => $object->title,
                obj => $object,
            };
        }
    };

    $b->recurse ($sub);
}

# http://www.cyanwerks.com/file-format-url.html
sub bookmark_to_favorite {
    my $link = shift;

    my $obj = Config::IniFiles->new ();
    $obj->AddSection ('DEFAULT');
    $obj->newval ('DEFAULT', 'BASEURL', $link->{obj}->href);
    $obj->AddSection ('InternetShortcut');
    $obj->newval ('InternetShortcut', 'URL', $link->{obj}->href);

    return $link->{obj}->title, $obj;
}

sub favorite_to_bookmark {
    my $link = shift;

    my $obj = Netscape::Bookmarks::Link->new ({
        TITLE         => $link->{title},
        DESCRIPTION   => '',
        HREF          => $link->{obj}->val ('InternetShortcut', 'URL'),
        ADD_DATE      => '',
        LAST_VISIT    => '',
        LAST_MODIFIED => '',
        ALIAS_ID      => '',
    });

    return $obj;
}

1;
__END__

=head1 NAME

URI::Collection - Input and output link collections in different
formats.

=head1 SYNOPSIS

  use URI::Collection;

  $store = URI::Collection->new (
      file      => $bookmarks,
      directory => $favorites,
  );

  $file_contents = $store->as_bookmark_file ();
  $top_directory = $store->as_favorite_directory ();

=head1 ABSTRACT

Input and output link collections in different formats.

=head1 DESCRIPTION

An object of class URI::Collection represents a parsed Netscape style
bookmark file or a Windows "Favorites" directory with multi-format 
output methods.

=head1 METHODS

=head2 new

  $store = URI::Collection->new (
      file      => $bookmarks,
      directory => $favorites,
  );

Return a new URI::Collection object.

This method mashes the link store formats together, simultaneously.

=head2 as_bookmark_file

  $bookmarks = $store->as_bookmark_file ();

Output a Netscape style bookmark file.

=head2 as_favorite_directory

  $favorites = $store->as_favorite_directory ();

Output an M$ Windows "Favorites" folder.

=head1 DEPENDENCIES

L<Cwd>

L<File::Find>

L<File::Path>

L<Config::IniFiles>

L<Netscape::Bookmarks>

=head1 TODO

Make tests damnit!

Add a "save as" filename argument (and function) to the 
as_bookmark_file () method.

Return the M$ Favorites directory structure (as a variable) instead
of writing it to disk.

Handle the top Favorites path better!

Allow munging of file and directory handles to bookmark/favorites.

Throw out redundant links.

Allow slicing of the category-links structure.

Add a method to munge a set of links as bookmarks or favorites after 
the constructor is called.

Allow this link munging to happen under a given category.

Check if links are active.

Update link titles and URLs if changed or moved.

Mirror links?

Handle other bookmark formats (if there even are any) and "raw" lists
of links, to justify such a generic package name.  :-)

Make the internal hash structure an object attribute, instead of a 
global variable.

=head1 AUTHOR

Gene Boggs E<lt>cpan@ology.netE<gt>

=head1 COPYRIGHT AND LICENSE

Copyright 2003 by Gene Boggs

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself. 

=cut
