# Copyright 2002-2008 Josh Clark and Global Moxie, LLC. This code cannot be
# redistributed without permission from globalmoxie.com.  For more
# information, consult your Big Medium license.
#
# $Id: Section.pm 3043 2008-03-31 14:00:38Z josh $

package BigMed::Section;
use strict;
use warnings;
use utf8;
use Carp;
use base qw(BigMed::Data);
use BigMed::DiskUtil qw(bm_confirm_dir bm_move_dir bm_delete_dir);

###########################################################
# SET SECTION DATA SCHEMA
###########################################################

my @section_schema = (
    {   name    => 'site',
        type    => 'system_id',
        index   => 1,
        default => q{},
    },
    {   name    => 'name',
        type    => 'simple_text',
        index   => 1,
        default => q{},
    },
    {   name    => 'slug',
        type    => 'url_safe',
        index   => 1,
        unique  => 1,
        default => q{},
    },
    {   name     => 'parents',
        type     => 'system_id',
        multiple => 1,
        index    => 1,
        default  => [],
    },
    {   name     => 'kids',
        type     => 'system_id',
        multiple => 1,
        index    => 1,
        default  => [],
    },
    {   name  => 'page',
        type  => 'system_id',
        index => 1,
    },
    {   name    => 'active',
        type    => 'boolean',
        default => 1,
    },
    {   name    => 'alias',
        type    => 'url',
        default => q{},
    },
    {   name    => 'flags',
        type    => 'key_boolean',
        default => {},
    },
);

BigMed::Section->set_schema( elements => \@section_schema );

###########################################################
# CALLBACKS
###########################################################

BigMed::Section->add_callback( 'before_save',      \&_before_save );
BigMed::Section->add_callback( 'after_save',       \&_after_save );
BigMed::Section->add_callback( 'before_trash',     \&_before_trash );
BigMed::Section->add_callback( 'before_trash_all', \&_before_trash_all );

sub _before_save {
    my $section = shift;
    $section->update_id() or return;

    #check slug uniqueness if we have one
    if ( $section->slug ) {
        defined( my $unique = $section->is_unique('slug') ) or return;
        if ( !$unique ) { #reset slug with any previous value and return error
            my $app = BigMed->bigmed->app;
            return $app->set_error(
                text => [
                    'BM_Not unique',
                    $app->language('BM_slug name'),
                    $app->language('BM_section')
                ],
            );
        }
    }
    
    #create an associated page
    $section->section_page_obj() or return;
    return 1;
}

sub _after_save {
    my $section = shift;

    #make sure parent object has section as a child
    my @parents = $section->parents;
    return 1 if !@parents || $section->is_homepage || !$section->site;
    my $pid = $parents[-1] or return 1;

    defined( my $parent =
          BigMed::Section->fetch( { site => $section->site, id => $pid } ) )
      or return;

    if ($parent) {
        my @kids = $parent->kids;
        my %has_child = map { $_ => 1 } @kids;
        if ( !$has_child{ $section->id } ) {
            push @kids, $section->id;
            $parent->set_kids( \@kids );
            $parent->save or return;
        }
    }

    return 1;
}

sub update_html_directory {
    my ( $section, $orig_slug ) = @_;

    my $nslug = $section->slug;
    return 1 if !defined $nslug || $nslug eq q{} || $nslug eq $orig_slug;

    my $sid = $section->site or return 1;
    require BigMed::Site;
    defined( my $site = BigMed::Site->fetch($sid) ) or return;
    if ($site) {
        my $new_dir = $site->directory_path($section) or return;
        ( my $old_dir = $new_dir ) =~ s/([\/\\])\Q$nslug\E$/$1$orig_slug/ms;
        if ( -e $old_dir ) {
            bm_move_dir( $old_dir, $new_dir, { build_path => 1 } )
              or return;
        }
        else {
            bm_confirm_dir( $new_dir, { build_path => 1 } ) or return;
        }
    }
    return 1;
}

sub _before_trash {
    my $section = shift;

    #bail out if no site or no id, let trash give an error
    return 1 if !$section->id || !$section->site;

    defined( my $site = BigMed::Site->fetch( $section->site ) ) or return;
    if ( !$site ) {
        warn 'No site object #'
          . $section->site
          . ' found when tried to trash section '
          . $section->id;
        return 1;    #fail quietly
    }
    my @missing_kids = $site->all_descendants_ids($section);
    my $rinfo        = {
        site         => $site,
        sections     => [$section->id],
        missing_kids => ( scalar @missing_kids ? \@missing_kids : undef ),
    };
    
    #have to save as inactive to avoid building and re-creating a new
    #page object for the section along the way (content items trigger
    #rebuilds when they're deleted downstream). Otherwise you get
    #an orphan section object.
    $section->set_active(undef);
    $section->save() or return;
    return _trash_related_objects( $section->site, $rinfo );
}

sub _before_trash_all {
    my $select = shift;
    return 1 if !$select->count;    #none to trash
    
    #have to save as inactive to avoid building and re-creating a new
    #page object for the section along the way (content items trigger
    #rebuilds when they're deleted downstream). Otherwise, you get
    #an orphan section object
    my $sec;
    while ($sec = $select->next) {
        $sec->set_active(undef);
        $sec->save() or return;
    }

    # the selection object may contain sections from multiple sites,
    # and it also many not contain some subsections of the sections that
    # are in the selection (all subsections of deleted sections should also
    # be removed).
    #
    # so we gather this info for each site in the selection and stow
    # in a hash keyed to site id:
    # 1. site: the site object
    # 2. sections: an unordered array of the section IDs in the selection
    # 3. missing_kids: an unordered array of any missing subsections of
    #    the sections in the selection (undef if no missing kids).

    my %site_info = _section_info_from_select($select);
    while ( my ( $siteid, $rinfo ) = each(%site_info) ) {
        _trash_related_objects( $siteid, $rinfo ) or return;
    }
    return 1;
}

sub _trash_related_objects {
    my ( $siteid, $rinfo ) = @_;

    #delete the related objects for the items in the selection for the
    #current site and info in the $rinfo hash. Return true on success,
    #false on error.

    #if there are sections listed in the missing_kids parameter of $rinfo,
    #it bails out by creating a new selection with all sections and
    #calling trash_all on those. (trash_all will still get called on
    #the original set of sections, but they will have already been deleted).

    my @kill_secs = @{ $rinfo->{sections} };    #already in selection
    if ( $rinfo->{missing_kids} ) {
        push @kill_secs, @{ $rinfo->{missing_kids} };
        my $all_sec =
          BigMed::Section->select( { site => $siteid, id => \@kill_secs } )
          or return;
        return $all_sec->trash_all;
    }

    #delete any content that belongs exclusively to these sections;
    #for those that belong to other sections, update the objects to
    #remove the deleted sections.

    my %kill = map { $_ => 1 } @kill_secs;
    my %criteria = ( site => $siteid, sections => \@kill_secs );
    foreach my $class ( BigMed::Plugin->load_content_types ) {
        my $content = $class->select( \%criteria ) or return;
        _remove_content_from_sections( $content, $siteid, \%kill )
          or return;
    }

    #delete preference objects
    my $prefs =
      BigMed::Prefs->select( { site => $siteid, section => \@kill_secs } )
      or return;
    $prefs->trash_all or return;

    #step through all of the sections to find the topmost parents
    #(parent sections not already part of the selection), deleting
    #the html directories along the way.
    my %pid;
    my $site_obj = $rinfo->{site};
    foreach my $sec_id (@kill_secs) {
        defined( my $sec = $site_obj->section_obj_by_id($sec_id) )
          or return;
        next if !$sec;    #hm...

        #locate parent and mark to update if it's not one of the sections
        #we're deleting
        my $pid = ( $sec->parents )[-1];
        $pid{$pid} = 1 if $pid && !$kill{$pid};

        #delete the html directory; a bit duplicative since
        #some sections may be children of others in this group, but
        #bm_delete_dir is tolerant of non-existent directories so shouldn't
        #be a huge deal -- pretty minor performance issue
        my $html_dir = $site_obj->directory_path($sec) or return;
        bm_delete_dir($html_dir) or return;
    }

    #update parents to remove the sections to delete
    _remove_sections_from_parents( $site_obj, [keys %pid], \%kill ) or return;

    return 1;
}

sub _section_info_from_select {
    my $select = shift;

    # Organizes section and site info for the sections in the selection.
    # Returns a hash, keyed to each unique site id. Each value is a hash
    # with the following key/value pairs:
    # * 'site'         => $site_object
    # * 'sections'     => ref to unsorted array of IDs of sections in select
    # * 'missing_kids' => ref to unsorted array of IDs of any subsections of
    #                     sections in select that are not already in the
    #                     selection. This value is undef if a site does not
    #                     have any missing kids.

    require BigMed::Site;
    my %section_info;
    my %child_sec;
    my $index_orig = $select->index;
    $select->set_index(0);
    my $section;
    while ( $section = $select->next ) {
        my $sid = $section->site or next;
        $section_info{$sid}         ||= {};
        $section_info{$sid}->{site} ||= BigMed::Site->fetch($sid);
        return if !defined $section_info{$sid}->{site};

        #mark section as part of selection and collect its children
        push @{ $section_info{$sid}->{sections} }, $section->id;
        my @kids = $section_info{$sid}->{site}->all_descendants_ids($section);
        push( @{ $child_sec{$sid} }, @kids ) if @kids;
    }
    $select->set_index($index_orig);    #return index in same state
    return if !defined $section;

    #eliminate child section dupes and plug into the section_info hash
    while ( my ( $sid, $rkids ) = each(%child_sec) ) {
        my %found_sec = map { $_ => 1 } @{ $section_info{$sid}->{sections} };
        my %missing =
          map { $_ => 1 } grep { !$found_sec{$_} } @{$rkids};
        $section_info{$sid}->{missing_kids} = [keys %missing] if %missing;
    }
    return %section_info;
}

sub _remove_content_from_sections {
    my ( $content_select, $siteid, $rkill_secs ) = @_;

    #scan a selection of content objects and de-assign from any
    #sections whose IDs are keys in the $rkill_secs hash ref.
    #delete any objects that are no longer assigned to any sections.
    #returns true on success, undef on error.

    my @delete_ids;
    my $item;
    $content_select->set_index(0);
    while ( $item = $content_select->next ) {
        my @orig = $item->sections;
        my @secs = grep { !$rkill_secs->{$_} } @orig;
        if (@secs) {    #belongs to other sections
            if ( @secs < @orig ) {    #there was a change, update
                $item->set_sections( \@secs );
                $item->save or return;
            }
            next;
        }
        push @delete_ids, $item->id;
    }
    return 1 if !@delete_ids;

    #delete the content objects (BigMed::Content handles removing
    #all related objects and links)
    $content_select =
      $content_select->select( { site => $siteid, id => \@delete_ids } )
      or return;
    return $content_select->trash_all;
}

sub _remove_sections_from_parents {
    my ( $site_obj, $rpid, $rkill ) = @_;

    #fetches sections whose IDs are in the $rpid array ref and removes
    #any sections whose IDs are keyed in the $rkill hash array. Returns
    #true on success, false on error.

    foreach my $pid ( @{$rpid} ) {
        defined( my $sec = $site_obj->section_obj_by_id($pid) ) or return;
        next if !$sec;    #hm... no such parent object
        my @kids = $sec->kids;
        my @new_kids = grep { !$rkill->{$_} } @kids;
        if ( @new_kids < @kids ) {    #update if there's a change
            $sec->set_kids( \@new_kids );
            $sec->save or return;
        }
    }
    return 1;
}

###########################################################
# HOMEPAGE METHODS
###########################################################

sub make_homepage {
    my $section = shift;
    $section->set_parents( [0] );
    return 1;
}

sub is_homepage {
    my @parents = $_[0]->parents;
    return defined $parents[0] && $parents[0] == 0;
}

###########################################################
# SECTION PAGE
###########################################################

sub section_page_obj {
    require BigMed::Content::Page;
    my $section = shift;
    if ( !$section->id ) {
        $section->update_id or return;
    }
    my $site_id = $section->site
      or croak 'section must have a site id to call section_page_obj';
    my $page_id = $section->page;
    my $page;
    if ($page_id) {    #fetch it
        defined(
            $page = BigMed::Content::Page->fetch(
                {   site => $site_id,
                    id   => $page_id,
                }
            )
          )
          or return;

        #check section page after instead of part of search -- faster.
        return $page if $page && ( $page->sections )[0] == $section->id;
    }

    #no matching page exists, build one and save
    $page = BigMed::Content::Page->new();
    $page->update_id() or return;
    $page->set_title( $section->name || 'Untitled' );
    $page->set_sections( [$section->id] );
    $page->set_site($site_id);
    $page->set_subtype('section');
    $page->set_pub_status('published');
    $page->set_pub_time( BigMed->bigmed_time );
    $section->set_page( $page->id );
    $page->save or return;
    $section->save or return;

    #queue for search index
    require BigMed::Search::Scheduler;
    import BigMed::Search::Scheduler;
    schedule_index($site_id, $page) or return;
    
    return $page;
}

1;
__END__


=head1 NAME

BigMed::Section - Big Medium section record

=head1 DESCRIPTION

A BigMed::Section object represents a content section/category of a website in
the Big Medium system.

=head1 USAGE

BigMed::Section is a subclass of BigMed::Data. In addition to the methods 
documented below, please see the BigMed::Data documentation for details about
topics including:

=over 4

=item Creating a new data object

=item Saving a data object

=item Finding and sorting saved data objects

=item Data access methods

=item Error handling

=back

=head1 METHODS

=head2 Data Access Methods

The BigMed::Site object holds the following pieces of data. They can be
accessed and set using the standard data access methods described in the
BigMed::Data documentation. See the L<"Searching and Sorting"> section below
for details on the data columns available to search and sort BigMed::Site
objects.

=over 4

=item * id

The numeric ID of the section

=item * name

The name of the section

=item * site

The numeric id of the site to which this section belongs

=item * slug

The section's slug name

=item * parents

Array of numeric ids of all of the
section's parent sections, starting with the top-most section of the site.

For example, consider a sub-sub-section named "High School" that is nested within
a site like so: "Home>Sports>Basketball>High School." The "High School" section's
parents array would consist of the IDs of the homepage section, the "Sports"
section, then the "Basketball" section.

    @parents = $section->parents();

=item * kids

Array of numeric ids of all of the
section's immediate child sections, in the order in which they should be listed
on the site.

    @kids = $section->kids();

=item * page

BigMed::Content object with the page-specific content data to display on the
section's main page.

=item * active

The boolean (Y or N) value indicating whether the section and its content should
be made available to the public

=item * alias

URL for this section if the section is an "aliased" section that points to
an external URL.

=item * flags

B<Hash reference.> A reference to the hash of section flags specified by the
various BigMed::Format subclasses. The key is the BigMed::Format flag name
and the value is true or false. See the BigMed::Format subclasses for details.

=item * mod_time

The time when the site object was last saved to disk, in UTC (Greenwich Mean)
time. The format: YYYY-MM-DD HH:MM:SS

=item * create_time

The time when the site object was first saved to disk, in UTC (Greenwich Mean)
time. The format: YYYY-MM-DD HH:MM:SS

=back

=head3 When You Save a Section Object

When a BigMed::Section object is saved with the C<save> method, a few
additional actions automagically happen, too:

=over

=item * The slug name is checked for uniqueness and, if not unique, the
save method returns undef, the section is not saved, and the slug name is
reverted to its original value.

=item * A BigMed::Content::Page object is created to act as the main section
page, if none already exists.

=item * If the BigMed::Section object has a parent, the parent BigMed::Section
object is updated to include the object ID in its C<kids> column value
if it does not already include it.

=back

=head3 C<trash> and C<trash_all> methods

When the C<trash> and C<trash_all> data methods are called on BigMed::Section
objects, a few additional steps are taken in addition to deleting the object
from the data store:

=over 4

=item * Any subsections of the section are also trashed.

=item * Any content in the section(s) is trashed (along with any related
objects for that content). Content that belongs to other sections in addition
to the deleted section(s) is updated so that it is no longer assigned
to the deleted section(s).

=item * Any preferences associated with the trashed section(s) are also
trashed.

=item * The parent object for the deleted section(s) is updated to remove
the deleted section as a child.

=item * The html directory for the section is deleted from the page directory.

=back

=head3 Searching and Sorting

You can look up and sort records by any combination of the following fields.
See the C<fetch> and C<select> documentation in BigMed::Data for more info.

=over 4

=item * id

=item * name

=item * site

=item * slug

=item * parents

=item * kids

=item * mod_time

=back

=head2 Homepage Methods

The homepage section is the top-most section of a site, the parent section
of all the rest.

=over 4

=item * $section->make_homepage()

Marks a section as a homepage section (does not do enforcement to make sure that
no other sections are marked as the homepage).

=item * $section->is_homepage()

Returns some true value if the section is a homepage.

=back

=head2 Content Page

Every section object has an associated BigMed::Content::Page object,
which controls the content that gets displayed on the front page of
the section.

=over 4

=item * C<< $section->section_page_obj() >>

Returns the section's page object (or undef on error). If no
page object exists, the method creates a page object and assigns
the new page's ID to the section's page column. (The method
saves the new page object to disk and, if necessary, the section
object, too).

=back

=head2 Public "Slug" Directory

Big Medium stores all of the public files (html pages, rss feeds, etc)
for the section's content in its slug directory. If a slug changes,
you can move the slug directory and all contained files via this
method:

=over 4

=item * C<< $section->update_html_directory($old_slug) >>

Changes the name of the slug directory from the old slug name in the argument
to the new slug in the section. Returns true on success.

=back

=head1 SEE ALSO

=over 4

=item * BigMed::Data

=back

=head1 AUTHOR & COPYRIGHTS

This module and all Big Medium modules are copyright Josh Clark
and Global Moxie. All rights reserved.

Use of this module and the Big Medium content
management system are governed by Global Moxie's software licenses
and may not be used outside of the terms and conditions outlined
there.

For more information, visit the Global Moxie website at
L<http://globalmoxie.com/>.

Big Medium and Global Moxie are service marks of Global Moxie
and Josh Clark. All rights reserved.

=cut

