#!/usr/bin/perl

use strict;
use warnings;

package Gallery;
use Template;
use DBI;
use Carp qw(confess);
use File::Copy qw(copy);
use POSIX qw(ceil);
#use File::Slurp;
use vars qw{$AUTOLOAD};

sub new {
	my ($class, @params) = @_;
	my $app = bless {
		templatedir => 'templates',
		info => {},
		@params
	}, $class;
	$app->connect;
	return $app;
}

sub checkcgi {
	my ($class, $q, @params) = @_;
	my $app = $class->new(@params);
	my $sid = $q->cookie('SID');
	if ($sid) {
		eval {
			$app->{user} = $app->checksession($sid);
		};
		if ($@ and $@ eq 'expired') {
			warn "session $sid expired";
			return $app;
		}
		if ($@) {
			warn "checksession: $@";
		}
		$app->{msg} = $@;
		$app->{SID} = $sid;
	}
	$app->commit;
	return $app;
}

sub validatecgi {
	my ($class, $q, @params) = @_;
	my $app = $class->new(@params);
	my $sid = $q->cookie('SID');
	if ($sid) {
		eval {
			$app->{user} = $app->validatesession($sid,undef);
		};
		if ($@ and $@ eq 'expired') {
			return $app;
		}
		if ($@) {
			warn "checksession: $@";
		}
		$app->{msg} = $@;
		$app->{SID} = $sid;
	}
	$app->commit;
	return $app;
}

sub newcgi {
	my ($class, $q, @params) = @_;
	my $app = $class->new(@params);
	my $sid = $q->cookie('SID');
	if ($sid) {
		eval {
			$app->{user} = $app->checksession($sid);
			warn "user = ", $app->{user};
		};
		if ($@ and $@ eq 'expired') {
			warn "session $sid expired";
			return $app;
		}
		if ($@) {
			warn "checksession: $@";
		}
		$app->{msg} = $@;
		$app->{SID} = $sid;
	}
	$app->commit;
	return $app;
}

sub logout {
	my ($app) = @_;
	if ($app->{SID}) {
		return $app->fetch('select * from close_session(?)', $app->{SID});
	}
	return 0;
}

sub file_info {
	my ($app, $dir, $file) = @_;
	my %info;
	my $path = "$dir/$file";
	$info{path} = $path;
	if (-e $path) {
		$info{path} = $path;
		$info{name} = $path; $info{name} =~ s|^.*/||;
		$info{stat} = [ stat $path ];
	} else {
		warn "$path doesn't exist";
		$info{name} = $path;
	}
	if (-f "$dir/thumb/$path") {
		$info{thumb} = "thumb/$file";
	} else {
		$info{thumb} = "thumb/$file";
	}
	$info{thumb} =~ s|(?i)\.jpg$|.png|;
	return \%info;
}

sub insert_hash {
	my ($app, $table, %hash) = @_;
	my @keys = keys %hash;
	my @values = @hash{@keys};
	my @ph = (('?') x @keys);
	local $" = ',';
	my $sql = qq{insert into $table (@keys) values (@ph) returning *};
	$app->fetch($sql, @values);
}

sub update_hash {
	my ($app, $table, $key, $value, %hash) = @_;
	return unless keys %hash;
	my @keys = keys %hash;
	my @values = @hash{@keys};
	my @ph = map { "$_ = ?" } @keys;
	local $" = ',';
	my $sql = qq{update $table set @ph where $key = ? returning *};
	$app->fetch($sql, @values, $value);
}

sub addcomment {
	my ($app, $image, $comment) = @_;
	$comment =~ s/^\s+//;
	$comment =~ s/\s+$//;
	return unless length $comment;
	$app->insert_hash('comments',
		media => $image,
		owner => $app->account,
		commenter => $app->user,
		comment => $comment
	);

	# TODO this should really be an internal message with
	# another program set up to email them
	my $ui = $app->userinfo($app->account);
	eval {
		if ($ui->{email}) {
			warn "sending email to ", $ui->{email};
			$app->{accountinfo} = $ui;
			$app->{userinfo} = $app->userinfo;
			$app->{imageinfo} = $app->fetchhash(q{
				select * from media where owner = ? and id = ?
				}, $app->account, $image);
			$app->{newcomment} = $comment;
			my $t = $app->t;
			open(my $email, '|/usr/sbin/sendmail -t') or die;
			$t->process('commentemail.tmpl', $app, $email) || die $t->error;
			close($email);
		} else {
			warn "not sending email";
		}
	};

}

sub new_media {
	my ($app, %info) = @_;
	$app->insert_hash('media', %info);
}

sub delete {
	my ($app, $image) = @_;
	warn sprintf('del user = %s, id = %s', $app->user, $image);
	$app->fetch(q{delete from media where owner = ? and id = ? returning *},
		$app->user, $image);
}

sub imageinfo {
	my ($app, $account, $tag) = @_;
	my $user = $app->{user};
	$user ||= 'public';
	$account ||= $user;

	my @image;
	if (defined $tag) {
		@image = $app->hash_list(q{
			select *
		       	from media where valid and owner = ? and tags @> ARRAY[?] order by uploaded desc, filename}, $account, $tag);
	} else {
		@image = $app->hash_list(q{
			select *
		       	from media where valid and owner = ? order by uploaded desc, filename}, $account);
	}

	$app->{info}->{image} = \@image;
}

sub userinfo {
	my ($app, $account) = @_;
	$account = $app->user unless defined $account;
	$app->fetchhash(q{select *, coalesce(nullif(gecos,''),username) as name from accounts where username = ?}, $account);
}

sub selfuser {
	my ($app) = @_;
	return $app->{user} eq $app->{account};
}

sub info {
	my ($app, $account, %filter) = @_;
	my $user = $app->{user};
	$user ||= 'public';
	$account ||= $user;

	my @params = ($account);
	my $sql = q{ select * from media where valid and owner = ?  };
	if ($filter{tag}) {
		warn sprintf qq{filter tag = '%s'\n}, $filter{tag};
		$sql .= ' and tags @> ARRAY[?]';
		push @params, $filter{tag};
		$app->{info}->{tag} = $filter{tag};
	}

	my $imagecount = $app->fetch(q{select count(*) from media where
		owner = ? and valid}, $account);
	$app->{info}->{mediacount} = $imagecount;

	$sql .= ' order by uploaded desc, caption, filename';

	if ($filter{pagesize}) {
		$filter{page} ||= 1;
		$filter{limit} = $filter{pagesize};
	}
	if ($filter{page}) {
		$filter{pagesize} ||= 20;
		$app->{info}->{page}->{size} = $filter{pagesize};
		$app->{info}->{page}->{page} = $filter{page};
		my $pcount = $filter{tag} ?
			$app->fetch(q{select count(*) from
				media where valid and owner = ?
					and tags @> ARRAY[?]}, @params)
			: $imagecount
			;

		$app->{info}->{page}->{pages} = ceil($pcount / $filter{pagesize});
		$filter{offset} = ($filter{page} - 1) * $filter{pagesize}
	}

	if ($filter{offset}) {
		$sql .= ' offset ?';
		push @params, $filter{offset};
	}
	if ($filter{limit}) {
		$sql .= ' limit ?';
		push @params, $filter{limit};
	}
	my @image = $app->hash_list($sql, @params);

	foreach my $i (@image) {
		my @comments = $app->hash_list(q{
			select * from comments where media = ? and owner = ?
			}, $i->{id}, $i->{owner}
		);
		$i->{comments} = \@comments;
	}

	$app->{info}->{image} = \@image;

	my @tags = $app->hash_list(q{select tag as "name", count from tagcounts where owner = ? order by count desc, tag}, $account);
	$app->{info}->{tags} = \@tags;

	warn "checking user '$user' vs account '$account'";
	my @gather = qw|video|;
	if ($user eq $account) {
		push @gather, qw|unknown incoming|;
	}
	for (@gather) {
		my $dir = "users/$account/$_";
		opendir(my $dh, $dir);
		my @files = grep { !/^\./ and -e "$dir/$_" and (-l "$dir/$_" or -f "$dir/$_") } readdir $dh;
		# my @files = grep { !/^\./ } readdir $dh;
		close $dh;
		$app->{info}->{$_} = [map { $app->file_info($dir,$_) } @files];
	}

	# TODO change this for acls
	if ($user eq $account) {
		my @wl = $app->hash_list(q{
			select W.watched as who,
		       	max(M.uploaded) as last from watchers W
			left join media M on W.watched = M.owner 
			where W.watcher = ?
				group by who
				order by last desc nulls last, who
				}, $user
			);
		$app->{watchlist} = \@wl;
	}
	if ($user ne $account) {
		$app->{inwatchlist} = $app->fetch(q{
			select count(*) = 1 from watchers
			where watcher = ? and watched = ?
			}, $user, $account
		);
	}

	return $app->{info};
}

sub addwatch {
	my ($app, @watch) = @_;
	for my $w (@watch) {
		$app->fetch(q{insert into watchers (watcher, watched) values (?, ?) returning *}, $app->{user}, $w);
	}
}

sub rmwatch {
	my ($app, @watch) = @_;
	for my $w (@watch) {
		$app->fetch(q{delete from watchers where watcher = ? and watched = ? returning *}, $app->{user}, $w);
	}
}

sub access {
	my ($app, $account, @path) = @_;
	return join('/', 'users', $account, @path);
}

# if(defined($ENV{'HTTP_IF_MODIFIED_SINCE'})){
# 	if ($ENV{'HTTP_IF_MODIFIED_SINCE'} == $last_modified){
# 		print header('text/html','304 Not Modified');
# 		return();
# 	} 
# }

use POSIX qw|strftime|;
sub sendfile {
	my ($app, $path) = @_;
	warn "sending $path";
	warn "no such file" unless -f $path || -l $path;
	unless (-f $path || -l $path) {
		print "Status: 404 Not Found\n\n";
		return 0;
	};
	my $mtime = (stat _)[9];
	my $lm = strftime("%a, %d %b %Y %T %z", gmtime $mtime);

	my $type = 'image/png';
	if ($path =~ /(?i)\.jpe?g$/) {
		$type = 'image/jpeg';
	} elsif ($path =~ /(?i)\.mov$/) {
		$type = 'movie/quicktime';
	}
	local $\ = "\n";
	print "Content-Type: $type";
	print "Last-Modified: $lm";
	print '';
	$| = 1;
	copy($path, \*STDOUT);
	1;
}


# Session management

# renew a session.  returns ?
# take a second parameter of for how long?
sub renewsession {
	my ($app, $sid) = @_;
	$app->fetch('select * from renew_session(?)', $sid);
}

sub exists {
	my ($app, $account) = @_;
	my @info = $app->hash_list(q{select * from accounts where username = ?},
		$app->{account});
	return ($info[0]);
}

sub setmedia {
	my ($app, $image, $field, $value, $account) = @_;
	my $sql = qq{update media set $field = ? where id = ? and owner = ?
		returning *
		};
	$account = $app->{account} unless defined $account;
	$app->fetch($sql, $value, $image, $account);
}

sub addfavorite {
	my ($app, $owner, @favorites) = @_;
	my $sql = qq{insert into favorites (username, owner, media) values (?,?,?) returning *};
	$app->fetch($sql, $app->{user}, $owner, $_) for @favorites;
}

sub unfavorite {
	my ($app, $owner, @favorites) = @_;
	my $sql = qq{delete from favorites where username = ? and owner = ? and media = ? returning *};
	$app->fetch($sql, $app->{user}, $owner, $_) for @favorites;
}

sub settags {
	my ($app, $image, @tags, $account) = @_;
	$account = $app->{account} unless defined $account;
	@tags = map { split(/,/); } @tags;
	@tags = map { s/^\s+//; s/\s+$//; $_ } @tags;
	@tags = grep { length } @tags;
	$app->fetch('update media set tags = ? where id = ? and owner = ? returning *', \@tags, $image, $account);
}

# returns valid, expire time, user
# could return no rows if no such session
sub checksession {
	my ($app, $sid) = @_;
	warn "checking session $sid";
	$app->fetch('select * from check_session(?)', $sid);
}

sub refreshsession {
	my ($app, $sid, $timeout) = @_;
	$sid ||= $app->{SID};
	$timeout ||= '4 hours';
	$app->fetch('update login set timeout = current_timestamp + ? returning *', $timeout);
}

sub validatesession {
	my ($app, $sid, $timeout) = @_;
	my $user;
	if ($timeout) {
		$user = $app->fetch('select * from validate_session(?,?)', $sid, $timeout);
	} else {
		$user = $app->fetch('select * from validate_session(?)', $sid);
	}
	if (!$user) {
		die "invalid session\n";
	}
	return $user;
}

# create a session, checking a password if supplied, returns SID
sub login {
	my ($app, $username, $password) = @_;
	my $sid;
	if (defined $password) {
		warn "checking $username $password";
		$sid = $app->fetch('select create_session(?,?)', $username, $password);
		warn "sid = $sid";
	} else {
		$sid = $app->fetch('select create_session(?)', $username);
	}
	$app->{user} = $username if $sid;
	warn "set user to ", $app->{user};
	return $sid;
}

# User management

sub setpassword {
	my ($app, $username, $password) = @_;
	$app->fetch('select set_password(?,?)', $username, $password);
}

sub listed {
	my ($app) = @_;
	if ($app->{user}) {
		return $app->hash_list(q{
			select *, watchers @> ARRAY[?] as watching
				from listedusers U
				order by U.gecos, U.username nulls last
			}, $app->user
		);
	} else {
		return $app->hash_list(q{
			select *
				from listedusers U
				order by U.gecos, U.username nulls last
			}
		);
	}
}

sub allusers {
	my ($app) = @_;
	return $app->hash_list(q{
		select *
			from allusers U
			order by U.gecos, U.username nulls last
		}
	);
}

sub create_user {
	my ($app, %user) = @_;
	my $user = $app->fetch('select create_user(?,?)', @user{qw(account password)});
	
	$app->fetch(q{update accounts set email = ? where username = ? returning *}, $user{email}, $user{account}) if $user{email};
	$app->fetch(q{update accounts set gecos = ? where username = ? returning *}, $user{name}, $user{account}) if $user{name};

	mkdir 'users';
	mkdir "users/$user";
	mkdir "users/$user/$_" for qw|library image music video incoming thumb unknown|;
	mkdir "users/$user/$_" for qw|image/thumb video/thumb|;
	mkdir "users/$user/$_" for qw|playlist|;
}

# Get a template object
sub t {
	my ($app) = @_;
	if (!$app->{t}) {
		$app->{t} = Template->new({INCLUDE_PATH=>$app->{templatedir}});
	}
	return $app->{t};
}

# lets auto-create accessors
sub AUTOLOAD {
	my ($app, @args) = @_;
	my $field = $AUTOLOAD;
	$field =~ s/.*:://;
	if (not exists $app->{$field}) {
		confess "undefined method $field called";
	}
	if (@args) {
		$app->{$field} = $_[0];
		return $app;
	}
	return $app->{$field};

# 	*$field = sub {
# 		my ($a, @arg) = @_;
# 		if ($arg[0]) {
# 			$a->{$field} = $arg[0];
# 			return $a;
# 		}
# 		return $a->{$field};
# 	}

}

# DBI delegates

sub commit {
	my ($a) = @_;
	if (not ref $a->{db}) {
		Carp::confess "no db";
	}
	$a->{db}->commit;
}
sub rollback {
	my ($a) = @_;
	if (not ref $a->{db}) {
		Carp::confess "no db";
	}
	$a->{db}->rollback;
}

sub connect {
	my ($a) = @_;
	if (!$a->{db}) {
		$a->{db} = DBI->connect('dbi:Pg:dbname=gallery', undef, undef,
			{ RaiseError => 1, AutoCommit => 0}
		);
	}
	die "can't connect to database" unless $a->{db};

	return $a->{db};
}

sub disconnect {
	my ($a) = @_;
	if ($a->{db}) {
		$a->{db}->disconnect;
	}
}

sub prepare {
	my ($a, @args) = @_;
	$a->{db}->prepare(@args);
}

# dbi utilities

# is this just select_all hashref?
sub hash_list {
	my ($a, $sql, @args) = @_;
        my $st = $a->prepare($sql);
	eval {
	        $st->execute(@args);
	};
	if ($@) {
		confess $@;
	}
        my @r = ();

        while (my $r = $st->fetchrow_hashref()) {
                push @r, $r;
        }
        $st->finish;
        return wantarray ? @r : [@r];
}

sub fetchhash {
        my ($a, $sql, @args) = @_;
	my $st;
	if (!ref $sql) {
        	$st = $a->{db}->prepare($sql);
	} else {
		$st = $sql;
	}
        $st->execute(@args);
        my (@r) = $st->fetchrow_hashref;
        $st->finish unless ref $sql;
        return wantarray ? @r : $r[0];
}

sub fetch {
        my ($a, $sql, @args) = @_;
	my $st;
	if (!ref $sql) {
        	$st = $a->{db}->prepare($sql);
	} else {
		$st = $sql;
	}
        $st->execute(@args);
        my (@r) = $st->fetchrow_array;
        $st->finish unless ref $sql;
        return wantarray ? @r : $r[0];
}

# Template delegates

sub process {
	my ($a, @args) = @_;
	my $t = $a->t;
	$t->process(@args);
}

# Image Queue

1;
