#
# Thingy
# A class for Thingy instances.
#
# (C) 2006-2011 Julian Mehnle <julian@mehnle.net>
# $Id$
#
##############################################################################

package Thingy;

use version; our $VERSION = qv('1.107');

use warnings; #no warnings 'once';
use strict;

use IO::File;
use IO::Dir;
use File::Path;
use Error ':try';

use Thingy::Exception;
use Thingy::Config;
use Thingy::Monolith;
use Thingy::Util;

use constant TRUE   => (0 == 0);
use constant FALSE  => not TRUE;

use constant INSTANCE_NAME_PATTERN  => qr/^[\w.-]+$/;
use constant HTTPD_CONF_FILE        => 'httpd.conf';

sub enumerate {
    my ($class, $name_glob) = @_;
    my @thingy_config_dirs = grep(-d, glob(Thingy::Config::CONFIG_INSTANCES_DIR . '/*'));
    my @thingy_instances = map(
        m#^${\Thingy::Config::CONFIG_INSTANCES_DIR}/(.+)$# ? $1 : (),
        @thingy_config_dirs
    );
    my @valid_thingy_instances = grep(eval { $class->get(name => $_) }, @thingy_instances);
    @valid_thingy_instances = Thingy::Util::glob_grep($name_glob, @valid_thingy_instances)
        if defined($name_glob);
    return sort(@valid_thingy_instances);
}

sub new {
    my ($class, %options) = @_;
    throw Thingy::EInvalidInstanceName('Invalid instance name specified: "' . $options{name} . '"')
        if $options{name} !~ INSTANCE_NAME_PATTERN;
    my $self = {
        load_level  => 0,
        %options
    };
    return bless($self, $class);
}

sub get {
    my ($class, %options) = @_;
    my $self = $class->new(%options);
    throw Thingy::EInstanceNotExists('Instance "' . $self->name . '" does not exist')
        if not -d $self->config_dir or not -d $self->database_dir;
    return $self;
}

sub create {
    my ($class, %options) = @_;
    Thingy::Util::require_superuser();
    
    require Thingy::Upgrade;
    require Thingy::Upgrade::Upgradables;
    
    my $self = $class->new(%options);
    
    throw Thingy::EInstanceExists('Cannot create instance "' . $self->name . '", instance already exists')
        if -e $self->config_dir or -e $self->database_dir;
    
    # Create main database directory:
    File::Path::mkpath($self->database_dir);
    
    # Create config directory from skeleton:
    Thingy::Util::copy_recursive(Thingy::Config::SKEL_ETC_DIR, $self->config_dir);
    
    # Create symlinks in 'static' dir:
    symlink(Thingy::Config::APPLICATION_WWW_DIR . '/' . $_, $self->static_dir . '/' . $_)
        foreach Thingy::Config::STATIC_DIR_SYMLINKS;
    
    # Register config files in upgradables catalog:
    foreach my $upgradable_name (Thingy::Upgrade::upgradables()->names) {
        my $upgradable = Thingy::Upgrade::Upgradable->new_from_file(
            undef,
            Thingy::Util::skel_to_instance_file($self, $upgradable_name),
            $upgradable_name
        );
        $self->upgradables->add($upgradable);
        $self->upgradables->commit();
    }
    
    # Create database component dirs:
    File::Path::mkpath([
        map(
            $self->database_component_path($_),
            map("user/$_", 0..9), qw(page keep temp blobs)
        )
    ]);
    
    $self->load(1);
    
    # Create skeleton pages:
    foreach my $page_template_filename (glob(Thingy::Config::SKEL_PAGES_DIR . '/*.wookee')) {
        my $page_name = $page_template_filename;
        $page_name =~ s#^${\Thingy::Config::SKEL_PAGES_DIR}/(.*)\.wookee$#$1# or next;
        $page_name =~ s#%#/#g;
        my $page_template_file = IO::File->new($page_template_filename)
            or warn("Cannot create page \"$page_name\" from skeleton template $page_template_filename: $!"),
            next;
        my $text = join('', $page_template_file->getlines());
        my $page = $self->page_class->create(instance => $self, name => $page_name);
        $page->text($text);
        $page->commit('system maintenance: created page', FALSE, TRUE);
    }
    
    # Set ownership and permissions recursively on database directory:
    Thingy::Util::chownmod_recursive(
        $self->database_dir, Thingy::Config::HTTPD_UID, Thingy::Config::HTTPD_GID, 0644);
    
    # Create httpd.conf snippet:
    my $file = IO::File->new($self->httpd_conf_file, '>')
        or throw Thingy::ENoCreateFile('Cannot create file ' . $self->httpd_conf_file . ": $!");
    $file->print(<<"EOF");
Alias /robots.txt           ${\Thingy::Config::APPLICATION_WWW_DIR}/robots.txt
Alias /static               ${\$self->static_dir}
Alias /blobs                ${\$self->database_dir}/blobs

#ScriptAlias /auth           /usr/lib/cgi-bin/thingy
ScriptAlias /               /usr/lib/cgi-bin/thingy/

<LocationMatch "^/">
    SetEnv THINGY_INSTANCE ${\$self->name}
#    SetEnv THINGY_RESTRICTED 1
#    SetEnv THINGY_AUTH_URL http://.../auth
    
#    SetEnv TZ UTC
</LocationMatch>

#<LocationMatch "^/auth(/|\$)">
#    AuthName "Authenticated Thingy"
#    AuthType Digest
#    AuthDigestDomain /auth
#    AuthUserFile ${\$self->config_dir}/htdigest
#    Require valid-user
#    
#    SetEnv THINGY_RESTRICTED 0
#</LocationMatch>
EOF
    $file->close();
    
    return $self;
}

sub clone {
    my ($self, %options) = @_;
    Thingy::Util::require_superuser();
    my $class = ref($self) || $self;
    my $clone = $class->new(%$self, %options);
    
    my  $self_config_dir   =  $self->config_dir;
    my $clone_config_dir   = $clone->config_dir;
    my  $self_database_dir =  $self->database_dir;
    my $clone_database_dir = $clone->database_dir;
    
    throw Thingy::EInstanceExists('Cannot create instance "' . $clone->name . '", instance already exists')
        if -e $clone_config_dir or -e $clone_database_dir;
    
    # Copy config directory:
    Thingy::Util::copy_recursive($self_config_dir, $clone_config_dir);
    
    # Adjust httpd config file:
    Thingy::Util::patch_file(
        $clone->httpd_conf_file,
        sub {
            # Adjust instance name:
            s{^(\s*)((SetEnv\s+THINGY_INSTANCE\s+)\Q${\$self->name}\E\s*)$}
             {$1#XXX# $2\n$1$3${\$clone->name}}gm;
            # Adjust config dir in non-comment lines:
            s{^(\s*)(?![\s#])(.*\Q$self_config_dir\E.*)$} {
                my ($indent, $new, $old) = ($1, $2, $2);
                $new =~ s/\Q$self_config_dir\E/$clone_config_dir/g;
                "$indent#XXX# $old\n$indent$new"
            }egm;
            # Adjust database dir in non-comment lines:
            s{^(\s*)(?![\s#])(.*\Q$self_database_dir\E.*)$} {
                my ($indent, $new, $old) = ($1, $2, $2);
                $new =~ s/\Q$self_database_dir\E/$clone_database_dir/g;
                "$indent#XXX# $old\n$indent$new"
            }egm;
        }
    );
    
    # Copy database directory and set ownership and permissions recursively:
    Thingy::Util::copy_recursive($self_database_dir, $clone_database_dir);
    Thingy::Util::chownmod_recursive(
        $clone_database_dir, Thingy::Config::HTTPD_UID, Thingy::Config::HTTPD_GID, 0644);
    
    return $clone;
}

sub delete {
    my ($self) = @_;
    throw Thingy::ENoClassMethod() if not ref($self);
    Thingy::Util::require_superuser();
    File::Path::rmtree($self->config_dir);
    File::Path::rmtree($self->database_dir);
    return;
}

sub upgrade {
    my ($self, $logger, $log_level) = @_;
    throw Thingy::ENoClassMethod() if not ref($self);
    Thingy::Util::require_superuser();
    $self->load(1);
    require Thingy::Upgrade;
    Thingy::Upgrade::upgrade_instance($self, $logger, $log_level);
    return;
}

sub load {
    my ($self, $load_level) = @_;
    throw Thingy::ENoClassMethod() if not ref($self);
    return if $self->{load_level} >= $load_level;
    if ($load_level >= 1 and $self->{load_level} < 1) {
        Thingy::Monolith::InitWiki($self);
        Thingy::Monolith::InitRequest();
    }
    $self->{load_level} = $load_level;
    return;
}

sub login {
    my ($self, $user_name, $user_id, $user_host) = @_;
    throw Thingy::ENoClassMethod() if not ref($self);
    # (TODO: Implement Session class? Derive from Instance class?)
    throw Thingy::Exception('User name or user ID required')
        if not defined($user_name) and not defined($user_id);
    try {
        my $user = $self->user($user_name, $user_id);
        $self->{user_name} = $user->name;
        $self->{user_id}   = $user->id;
    }
    catch Thingy::EUserNotExists with {
        my ($e) = @_;
        # If a user ID was explicitly specified, then there's simply no substitute
        # for that user, so we rethrow the exception:
        $e->throw if defined($user_id);
        # Otherwise, we go on with just the user name:
        $self->{user_name} = $user_name;
    };
    $self->{user_host} = $user_host;
    return;
}

sub logout {
    my ($self) = @_;
    throw Thingy::ENoClassMethod() if not ref($self);
    $self->{user_name} = undef;
    $self->{user_id}   = undef;
    $self->{user_host} = undef;
    return;
}

sub handle_request {
    my ($self) = @_;
    throw Thingy::ENoClassMethod() if not ref($self);
    $self->load(1);
    Thingy::Monolith::DoWikiRequest();
    return;
}

sub config_dir {
    my ($self) = @_;
    throw Thingy::ENoClassMethod() if not ref($self);
    return Thingy::Config::CONFIG_INSTANCES_DIR . '/' . $self->name;
}

sub config_file {
    my ($self) = @_;
    throw Thingy::ENoClassMethod() if not ref($self);
    return $self->config_dir . '/' . Thingy::Config::CONFIG_FILE;
}

sub config {
    my ($self, $option, @value) = @_;
    throw Thingy::ENoClassMethod() if not ref($self);
    return $self->{config} if not defined($option);
    my $varref;
    if (exists($self->{config}->{$option})) {
        # New-style config option:
        $varref = \$self->{config}->{$option};
    }
    else {
        # Legacy config option as global variable in Thingy::Monolith package:
        $self->load(1);
        no strict 'refs';
        $varref = \${"Thingy::Monolith::${option}"};
    }
    $$varref = $value[0] if @value;
    return $$varref;
}

sub database_dir {
    my ($self) = @_;
    throw Thingy::ENoClassMethod() if not ref($self);
    return Thingy::Config::DATABASE_BASE_DIR . '/' . $self->name;
}

sub database_component_path {
    my ($self, $component) = @_;
    throw Thingy::ENoClassMethod() if not ref($self);
    return $self->{database_component_path}->{$component} ||=
        $self->database_dir . '/' . $component;
}

sub httpd_conf_file {
    my ($self) = @_;
    throw Thingy::ENoClassMethod() if not ref($self);
    return $self->config_dir . '/' . HTTPD_CONF_FILE;
}

sub static_dir {
    my ($self) = @_;
    throw Thingy::ENoClassMethod() if not ref($self);
    return $self->config_dir . '/static';
}

sub version {
    my ($self) = @_;
    throw Thingy::ENoClassMethod() if not ref($self);
    # No version number is being stored with instances for now.
    return undef;
}

sub upgradables {
    my ($self) = @_;
    require Thingy::Upgrade::Upgradables;
    return $self->{upgradables} ||= Thingy::Upgrade::Upgradables->new(
        $self->database_component_path('upgradables')
    );
}

sub user_class {
    my ($self) = @_;
    $self->{user_class} ||= 'Thingy::User';
    eval("require $self->{user_class}")
        or throw Thingy::Exception("Cannot load user class $self->{user_class}");
    return  $self->{user_class};
}

sub page_class {
    my ($self) = @_;
    $self->{page_class} ||= 'Thingy::Page';
    eval("require $self->{page_class}")
        or throw Thingy::Exception("Cannot load page class $self->{page_class}");
    return  $self->{page_class};
}

sub users {
    my ($self, $user_name_glob) = @_;
    throw Thingy::ENoClassMethod() if not ref($self);
    $self->load(1);
    my @user_ids;
    foreach my $units_column (0..9) {
        my $user_db_path = $self->database_component_path('user') . '/' . $units_column;
        push(@user_ids, map(m#/(\d+)\.db$# && $1, glob($user_db_path . '/*.db')));
    }
    my @users = map($self->user_class->get(instance => $self, id => $_), @user_ids);
    if (defined($user_name_glob)) {
        my $user_name_regexp = Thingy::Util::glob_to_regexp($user_name_glob);
        @users = grep(($_->name || '') =~ $user_name_regexp, @users);
    }
    @users =
        sort {
            ($a->name || '') cmp ($b->name || '') or
             $a->id          <=>  $b->id
        } @users;
    return @users;
}

sub user {
    my ($self, $user_name, $user_id) = @_;
    throw Thingy::ENoClassMethod() if not ref($self);
    $self->load(1);
    return $self->user_class->get(instance => $self, name => $user_name, id => $user_id);
}

sub pages {
    my ($self, $page_name_glob) = @_;
    throw Thingy::ENoClassMethod() if not ref($self);
    $self->load(1);
    my @page_names = map(
        Thingy::Util::decode_page_name($_),
        Thingy::Monolith::GenerateAllPagesList()
    );
    @page_names = Thingy::Util::glob_grep($page_name_glob, @page_names)
        if defined($page_name_glob);
    my @pages = map($self->page_class->get(instance => $self, name => $_), @page_names);
    return @pages;
}

sub page {
    my ($self, $page_name) = @_;
    throw Thingy::ENoClassMethod() if not ref($self);
    $self->load(1);
    return $self->page_class->get(instance => $self, name => $page_name);
}

sub user_name {
    my ($self, @user_name) = @_;
    throw Thingy::ENoClassMethod() if not ref($self);
    $self->{user_name} = $user_name[0] if @user_name;
    return Thingy::Config::SYSTEM_USER_NAME
        if not defined($self->{user_name}) and not defined($self->{user_id});
    return $self->{user_name};
}

sub user_id {
    my ($self, @user_id) = @_;
    throw Thingy::ENoClassMethod() if not ref($self);
    $self->{user_id} = $user_id[0] if @user_id;
    return $self->{user_id} || 0;
}

sub user_host {
    my ($self, @user_host) = @_;
    throw Thingy::ENoClassMethod() if not ref($self);
    $self->{user_host} = $user_host[0] if @user_host;
    return $self->{user_host} || 'localhost';
}

# Read-only accessors, no loading required:
Thingy::Util::make_accessor(__PACKAGE__, $_, TRUE)
    foreach qw(load_level name restricted authenticated auth_url);

TRUE;

