package Statocles::Site;
# ABSTRACT: An entire, configured website
$Statocles::Site::VERSION = '0.040';
use Statocles::Base 'Class', 'Emitter';
use Scalar::Util qw( blessed );
use Mojo::URL;
use Mojo::DOM;
use Mojo::Log;
use Statocles::Page::Plain;
use Statocles::Page::File;


has title => (
    is => 'ro',
    isa => Str,
);


has base_url => (
    is => 'ro',
    isa => Str,
    default => sub { '/' },
);


has theme => (
    is => 'ro',
    isa => Theme,
    coerce => Theme->coercion,
    default => sub {
        Statocles::Theme->new( store => '::default' );
    },
);


has apps => (
    is => 'ro',
    isa => HashRef[InstanceOf['Statocles::App']],
    default => sub { {} },
);


has index => (
    is => 'ro',
    isa => Str,
    default => sub { '' },
);


has _nav => (
    is => 'ro',
    isa => LinkHash,
    coerce => LinkHash->coercion,
    default => sub { {} },
    init_arg => 'nav',
);


has build_store => (
    is => 'ro',
    isa => Store,
    default => sub {
        my $path = Path::Tiny->new( '.statocles', 'build' );
        if ( !$path->is_dir ) {
            # Automatically make the build directory
            $path->mkpath;
        }
        return Store->coercion->( $path );
    },
    coerce => sub {
        my ( $arg ) = @_;
        if ( !ref $arg && !-d $arg ) {
            # Automatically make the build directory
            Path::Tiny->new( $arg )->mkpath;
        }
        return Store->coercion->( $arg );
    },
);


has _deploy => (
    is => 'ro',
    isa => ConsumerOf['Statocles::Deploy'],
    required => 1,
    init_arg => 'deploy',
    coerce => sub {
        if ( ( blessed $_[0] && $_[0]->isa( 'Path::Tiny' ) ) || !ref $_[0] ) {
            require Statocles::Deploy::File;
            return Statocles::Deploy::File->new(
                path => $_[0],
            );
        }
        return $_[0];
    },
);


has data => (
    is => 'ro',
    isa => HashRef,
    default => sub { {} },
);


has log => (
    is => 'ro',
    isa => InstanceOf['Mojo::Log'],
    lazy => 1,
    default => sub {
        Mojo::Log->new( level => 'warn' );
    },
);

# The current deploy we're writing to
has _write_deploy => (
    is => 'rw',
    isa => ConsumerOf['Statocles::Deploy'],
    clearer => '_clear_write_deploy',
);


sub BUILD {
    my ( $self ) = @_;
    $Statocles::SITE = $self;
    for my $app ( values %{ $self->apps } ) {
        $app->site( $self );
    }
}


sub app {
    my ( $self, $name ) = @_;
    return $self->apps->{ $name };
}


sub nav {
    my ( $self, $name ) = @_;
    return $self->_nav->{ $name } ? @{ $self->_nav->{ $name } } : ();
}


sub build {
    my ( $self ) = @_;

    my $store = $self->build_store;

    # Remove all pages from the build directory first
    $_->remove_tree for $store->path->children;

    my $apps = $self->apps;
    my @pages;
    my %args = (
        site => $self,
    );

    # Collect all the pages for this site
    for my $app_name ( keys %{ $apps } ) {
        my $app = $apps->{$app_name};

        my @app_pages = $app->pages;
        if ( $self->index eq $app_name ) {

            die sprintf 'ERROR: Index app "%s" did not generate any pages' . "\n", $self->index
                unless @app_pages;

            # Rename the app's page so that we don't get two pages with identical
            # content, which is bad for SEO
            $app_pages[0]->path( '/index.html' );
        }

        push @pages, @app_pages;
    }

    # Rewrite page content to add base URL
    my $base_url = $self->base_url;
    if ( $self->_write_deploy ) {
        $base_url = $self->_write_deploy->base_url || $base_url;
    }
    my $base_path = Mojo::URL->new( $base_url )->path;
    $base_path =~ s{/$}{};

    for my $page ( @pages ) {
        my $content = $page->render( %args );

        if ( !ref $content ) {
            if ( $base_path =~ /\S/ ) {
                my $dom = Mojo::DOM->new( $content );
                for my $attr ( qw( src href ) ) {
                    for my $el ( $dom->find( "[$attr]" )->each ) {
                        my $url = $el->attr( $attr );
                        next unless $url =~ m{^/};
                        $el->attr( $attr, join "", $base_path, $url );
                    }
                }
                $content = $dom->to_string;
            }
        }

        $store->write_file( $page->path, $content );
    }

    # Build the sitemap.xml
    # html files only
    my @indexed_pages = grep { $_->path =~ /[.]html?$/ } @pages;
    my $tmpl = $self->theme->template( site => 'sitemap.xml' );
    my $sitemap = Statocles::Page::Plain->new(
        path => '/sitemap.xml',
        content => $tmpl->render( site => $self, pages => \@indexed_pages ),
    );
    push @pages, $sitemap;
    $store->write_file( 'sitemap.xml', $sitemap->render );

    # robots.txt is the best way for crawlers to automatically discover sitemap.xml
    # We should do more with this later...
    my $robots_tmpl = $self->theme->template( site => 'robots.txt' );
    my $robots = Statocles::Page::Plain->new(
        path => '/robots.txt',
        content => $robots_tmpl->render( site => $self ),
    );
    push @pages, $robots;
    $store->write_file( 'robots.txt', $robots->render );

    # Add the theme
    my $theme_iter = $self->theme->store->find_files();
    while ( my $theme_file = $theme_iter->() ) {
        my $fh = $self->theme->store->open_file( $theme_file );
        push @pages, Statocles::Page::File->new(
            path => join( '/', '', 'theme', $theme_file ),
            fh => $fh,
        );
        $store->write_file( Path::Tiny->new( 'theme', $theme_file ), $fh );
    }

    $self->emit( build => class => 'Statocles::Event::Pages', pages => \@pages );

    return;
}


sub deploy {
    my ( $self ) = @_;
    $self->_write_deploy( $self->_deploy );
    $self->build;
    $self->_deploy->deploy( $self->build_store );
    $self->_clear_write_deploy;
}


sub url {
    my ( $self, $path ) = @_;
    my $base    = $self->_write_deploy && $self->_write_deploy->base_url
                ? $self->_write_deploy->base_url
                : $self->base_url;

    # Remove index.html from the end of the path, since it's redundant
    $path =~ s{/index[.]html$}{};

    # Remove the / from both sides of the join so we don't double up
    $base =~ s{/$}{};
    $path =~ s{^/}{};

    return join "/", $base, ( $path || !$base ? ( $path ) : () );
}

1;

__END__

=pod

=head1 NAME

Statocles::Site - An entire, configured website

=head1 VERSION

version 0.040

=head1 SYNOPSIS

    my $site = Statocles::Site->new(
        title => 'My Site',
        nav => [
            { title => 'Home', href => '/' },
            { title => 'Blog', href => '/blog' },
        ],
        apps => {
            blog => Statocles::App::Blog->new( ... ),
        },
    );

    $site->deploy;

=head1 DESCRIPTION

A Statocles::Site is a collection of L<applications|Statocles::App>.

=head1 ATTRIBUTES

=head2 title

The site title, used in templates.

=head2 base_url

The base URL of the site, including protocol and domain. Used mostly for feeds.

This can be overridden by L<base_url in Deploy|Statocles::Deploy/base_url>.

=head2 theme

The L<theme|Statocles::Theme> for this site. All apps share the same theme.

=head2 apps

The applications in this site. Each application has a name
that can be used later.

=head2 index

The application to use as the site index. The application's individual index()
method will be called to get the index page.

=head2 nav

Named navigation lists. A hash of arrays of hashes with the following keys:

    title - The title of the link
    href - The href of the link

The most likely name for your navigation will be C<main>. Navigation names
are defined by your L<theme|Statocles::Theme>. For example:

    {
        main => [
            {
                title => 'Blog',
                href => '/blog',
            },
            {
                title => 'Contact',
                href => '/contact.html',
            },
        ],
    }

=head2 build_store

The L<store|Statocles::Store> object to use for C<build()>. This is a workspace
and will be rebuilt often, using the C<build> and C<daemon> commands. This is
also the store the C<daemon> command reads to serve the site.

=head2 deploy

The L<deploy object|Statocles::Deploy> to use for C<deploy()>. This is
intended to be the production deployment of the site. A build gets promoted to
production by using the C<deploy> command.

=head2 data

A hash of arbitrary data available to theme templates. This is a good place to
put extra structured data like social network links or make easy customizations
to themes like header image URLs.

=head2 log

A L<Mojo::Log> object to write logs to. Defaults to STDERR.

=head1 METHODS

=head2 BUILD

Register this site as the global site.

=head2 app( name )

Get the app with the given C<name>.

=head2 nav( name )

Get the list of links for the given nav. Each link is a L<Statocles::Link> object.

    title - The title of the link
    href - The href of the link

If the named nav does not exist, returns an empty list.

=head2 build

Build the site in its build location.

=head2 deploy

Deploy the site to its destination.

=head2 url( path )

Get the full URL to the given path by prepending the C<base_url>.

=head1 EVENTS

The site object exposes the following events.

=head2 build

This event is fired after the site has been built and the pages written to the
C<build_store>.

The event will be a
L<Statocles::Event::Pages|Statocles::Event/Statocles::Event::Pages> object
containing all the pages built by the site.

=head1 AUTHOR

Doug Bell <preaction@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2015 by Doug Bell.

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

=cut
