#!/usr/bin/perl

use strict;
use FCGI;
use JSON;
use DBI;
use GD::Simple;
use Data::Dumper;
use warnings;

$Data::Dumper::Sortkeys = 1;

my $conf = load_conf("../etc/autodoc.json");
my $dbh  = sqlconnect($conf->{sql});

my %map = (
	api_v1_POST_documents		=> \&api_v1_POST_documents,
	api_v1_POST_documents_id_data	=> \&api_v1_POST_documents_id_data,
	api_v1_GET_documents_id_image	=> \&api_v1_GET_documents_id_image,
	api_v1_GET_pages_image		=> \&api_v1_GET_pages_image,
	api_v1_GET_documents		=> \&api_v1_GET_documents,
	api_v1_GET_documents_id		=> \&api_v1_GET_documents_id,
	api_v1_GET_pages_id		=> \&api_v1_GET_pages_id,
	api_v1_PATCH_documents_id	=> \&api_v1_PATCH_documents_id,
);

my $request = FCGI::Request();

while($request->Accept() >= 0) {

	my $user   = $ENV{REMOTE_USER} || 'undefined';
	my $qs     = parse_querystring($ENV{QUERY_STRING});
	my $method = $ENV{REQUEST_METHOD};

	# QS sanity check
	if ( exists $qs->{id} && $qs->{id} !~ /^\d+$/ ) { fatal_api_error(400,"invalid id"); }
	if ( exists $qs->{maxWidth} && $qs->{maxWidth} !~ /^\d+$/ ) { fatal_api_error(400,"invalid maxWidth"); }
	if ( exists $qs->{maxHeight} && $qs->{maxHeight} !~ /^\d+$/ ) { fatal_api_error(400,"invalid maxHeight"); }
	if ( exists $qs->{pageSize} && $qs->{pageSize} !~ /^\d+$/ ) { fatal_api_error(400,"invalid pageSize"); }
	if ( exists $qs->{pageIndex} && $qs->{pageIndex} !~ /^\d+$/ ) { fatal_api_error(400,"invalid pageIndex"); }

	my $path   = [ split(/\//,$ENV{SCRIPT_NAME}) ] if exists $ENV{SCRIPT_NAME};
	shift(@{$path});

	my $post   = parse_post(
		\*STDIN,
		exists $ENV{CONTENT_LENGTH} ? $ENV{CONTENT_LENGTH} : 0,
		exists $ENV{CONTENT_TYPE}   ? $ENV{CONTENT_TYPE}   : 0
	);


	my($code, $type, $data) = process_query($method, $path, $qs, $post, $user);

	if ( defined $type ) {
		if ( $type eq 'application/json' ) {
			$data = to_json($data, { utf8 => 1, pretty => 1 });
		}
	}

	send_response($code, $type, $data);
}

sub send_response {
	my ($code, $type, $data) = @_;

	printf("Status: %s\r\n", $code);
	printf("Content-type: %s\r\n", $type) if defined $type;
	printf("Content-length: %i\r\n", length($data)) if defined $data;
	printf("\r\n");
	print $data if defined $data;
}

sub fatal_api_error {
	my($code,$type,$body)=api_error(@_);
	send_response($code, $type, $body);
	exit;
}


sub load_conf {
	my($file) = @_;

	my $x='';

	open(F,"$file") || fatal_api_error(500,"Failed to load configuration file");
	while(<F>) { $x.=$_; }
	close(F);

	return from_json($x);
}

sub process_query {
	my($method, $path, $qs, $post, $user) = @_;

	my ($api_version, $path_main, $path_id, $path_sub) = @{$path};

	return api_error(404, "Unknown API version") if !defined $api_version;
	return api_error(405, "Unknown METHOD") if !defined $method;
	return api_error(404, "Unknown API function") if !defined $path_main;

	my $func = 'api_' . $api_version . '_' . $method . '_' . $path_main;

	$func .= '_id' if defined $path_id;
	$func .= '_'.$path_sub if defined $path_sub;

	return $map{$func}->($path_id, $qs, $post, $user) if exists $map{$func};

	return api_error(404, "Invalid path/method combination");
}

sub db_get_document_object {
	my($id) = @_;

	my $document;
	my @pages;
	my @pageids;
	my @tags;

	my $q = sqlquery($dbh, "SELECT * FROM documents WHERE id = ?", $id);
	while(my $hash = $q->fetchrow_hashref()) { $document = $hash; }

	$q = sqlquery($dbh, "SELECT * FROM pages WHERE documentId = ?", $id);
	while(my $hash = $q->fetchrow_hashref()) { push @pages, $hash; push @pageids, $hash->{id}; }

	$q = sqlquery($dbh, "
		SELECT tags.tag AS tag
		FROM documents_tags LEFT JOIN tags ON documents_tags.tagId = tags.id
		WHERE documentId = ? ORDER BY tag", $id);
	while(my ($tag) = $q->fetchrow_array()) { push @tags, $tag; }

	my %lang;

	foreach my $page ( @pages ) {
		$q = sqlquery($dbh, "SELECT * FROM pages_lang WHERE pageId = ?", $page->{id});
		while(my $hash = $q->fetchrow_hashref()) {
			$lang{$hash->{language}}++;
		}
	}

	if ( defined $document ) {

		my $out = {
			id => $document->{id},
			pageId => [ @pageids ],
			name => $document->{name},
			created => $document->{created},
			owner => $document->{owner},
			status => $document->{status},
			languages => [ keys %lang ],
			tags => [ @tags ],
		};

		return (200, "application/json", $out);
	}

	return (404, "application/json", "");
}

sub db_get_page_object {
	my($id) = @_;

	my $out;
	my %lang;

	my $q = sqlquery($dbh, "SELECT * FROM pages_lang WHERE pageId = ?", $id);
	while(my $hash = $q->fetchrow_hashref()) {
		$lang{$hash->{language}}++;
	}



	$q = sqlquery($dbh, "SELECT * FROM pages WHERE documentId = ?", $id);
	while(my $hash = $q->fetchrow_hashref()) {
		$out = {
			id         => $id,
			documentId => $hash->{documentId},
			name       => $hash->{name},
			created    => $hash->{created},
			owner      => $hash->{owner},
			status     => $hash->{status},
			language   => [ keys %lang ],
		};
	}

	if ( exists $out->{id} ) {
		return (200, "application/json", $out);
	}

	return (404, "application/json", "");
}

sub get_page_image {
	my($id, $qs) = @_;

	my $wh;
	my $size;
	if ( exists $qs->{maxWidth} ) {
		$wh = 'w';
		$size = $qs->{maxWidth};
	}
	elsif ( exists $qs->{maxHeight} ) {
		$wh = 'h';
		$size = $qs->{maxWidth};
	}
	my $src;
	my $dst;

	if ( !defined $id ) {
		$src = sprintf("%s/%s",
			$conf->{path}{global}, $conf->{path}{error_img});
	}
	else {
		$src = sprintf("%s/%s/%s.jpeg",
			$conf->{path}{global}, $conf->{path}{original}, $id);
	}

	if ( defined $wh && defined $size ) {
		if ( defined $id ) {
			$dst = sprintf("%s/%s/%s-%s-%s.jpeg",
				$conf->{path}{global}, $conf->{path}{cache}, $id, $wh, $size);
		}
		else {
			$dst = sprintf("%s/%s/error_img-%s-%s.jpeg",
				$conf->{path}{global}, $conf->{path}{cache}, $wh, $size);
		}
	}

	my $imgfile;

	if ( !defined $dst ) {
		$imgfile = $src;
	}
	else {
		if ( ! -r $dst ) {
			my $cmd = sprintf("convert %s -resize %s%s %s",
				$src,
				$wh eq 'h' ? 'x' : '',
				$size,
				$dst
			);
			print STDERR "CMD=$cmd\n";
			system($cmd);
		}

		$imgfile = $dst;
	}

	my $img = '';
	open(IMG, $imgfile) || fatal_api_error(500,"Failed to generate image");
	binmode(IMG);
	while(<IMG>) { $img.=$_; }
	close(IMG);

	return $img;
}

# create an empty document object.
sub api_v1_POST_documents {
	my($id, $qs, $post, $user) = @_;

	my $q = sqlquery($dbh, "
		INSERT INTO documents
		SET
			owner = ?,
			status = 'nodata'
		", $user);
	$q = sqlquery($dbh, "SELECT LAST_INSERT_ID()");
	while(my($lastid) = $q->fetchrow_array()) {
		$id = $lastid;
	}

	return db_get_document_object($id);
}

sub api_v1_POST_documents_id_data { return api_error(501,"Not yet implemented"); }

# get document image/thumbnail
sub api_v1_GET_documents_id_image {
	my($id, $qs, $post, $user) = @_;

	my $pageid;

	my $q = sqlquery($dbh, "SELECT id FROM pages WHERE documentId = ? ORDER BY id DESC LIMIT 1", $id);
	while(my($myid)=$q->fetchrow_array()) {
		$pageid = $myid;
	}

	return api_v1_GET_pages_id_image($pageid, $qs, $post, $user);
}

# get page image/thumbnail
sub api_v1_GET_pages_id_image {
	my($id, $qs, $post, $user) = @_;
	return (200, "image/jpeg", get_page_image($id, $qs));

}

# get a list of document objects
sub api_v1_GET_documents {
	my($id, $qs, $post, $user) = @_;

	$qs->{pageSize}  = $conf->{query}{pageSize}  if !exists $qs->{pageSize};
	$qs->{pageIndex} = $conf->{query}{pageIndex} if !exists $qs->{pageIndex};

	my @out;

	my $q = sqlquery($dbh, "SELECT id FROM documents LIMIT ?,?",
		$qs->{pageSize} * $qs->{pageIndex},
		$qs->{pageSize});
	while(my ($id) = $q->fetchrow_array()) {
		my ($code, $ct, $body) = db_get_document_object($id);
		push @out, $body;
	}

	return (
		200,
		"application/json",
		\@out
	);
}

# get a single document object
sub api_v1_GET_documents_id {
	my($id, $qs, $post, $user) = @_;
	return db_get_document_object($id);
}

# get a specific page
sub api_v1_GET_pages_id {
	my($id, $qs, $post, $user) = @_;
	return db_get_page_object($id);
}

# change document properties
sub api_v1_PATCH_documents_id {
	my($id, $qs, $post, $user) = @_;

	if ( exists $qs->{addTags} ) {
		my $tags = get_array($qs->{addTags});

		foreach my $tag ( @{$tags} ) {
			sqlquery($dbh, "CALL tag_add(?,?)", $id, $tag);
		}
	}

	if ( exists $qs->{deleteTags} ) {
		my $tags = get_array($qs->{deleteTags});

		foreach my $tag ( @{$tags} ) {
			sqlquery($dbh, "CALL tag_del(?,?)", $id, $tag);
		}
	}

	if ( exists $qs->{name} ) {
		sqlquery($dbh, "UPDATE documents SET name = ? WHERE id = ?",
			$qs->{name}, $id);
	}


	return (200);
}

sub get_array {
	my($x) = @_;
	my @arr;

	if ( ref $x eq 'ARRAY' ) {
		@arr = @{$x};
	}
	else {
		@arr = [ $x ];
	}
	return \@arr;
}

sub api_error {
	my($code, $text)=@_;

	my %deftext = (
		"000" => "An error produced an internal error, cascading errors over errors",
		"404" => "No such API path"
	);

	$code = "000" if !defined $code;
	$text = $deftext{$code} if ( !defined $text || $code eq "000" );

	return ( $code, "text/plain", $text . "\r\n" );
}

sub parse_querystring {
	my($in) = @_;

	my %out;

	if ( defined $in && length $in ) {
		foreach(split(/&/,$in)) {
			my($name,$value) = split(/=/);
			$value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;

			# handle arrays
			if ( exists $out{$name} ) {
				if ( ! ref $out{$name} ) {
					my $old = $out{$name};
					$out{$name} = [ $old ];
				}
				push @{$out{$name}}, $value;
			}
			else {
				$out{$name}=$value;
			}
		}
	}
	return \%out;
}

sub parse_post {
	my($fd, $len, $ct) = @_;

	my $data = '';
	while ( $len > 0 ) {
		my $buf;
		my $read = read($fd, $buf, $len);
		$len -= $read;
		$data .= $buf;
	}

	if ( $ct eq 'application/json' ) {
		my $tmp = from_json($data);
		$data = $tmp;
	}

	return $data;
}

sub sqlconnect {
	my($sql) = @_;

	my $dsn = "DBI:mysql:database=$sql->{base};host=$sql->{host}";
	my $dbh = DBI->connect($dsn, $sql->{user}, $sql->{pass}) || \\
		fatal_api_error(500,"Failed to connect to database");

	return $dbh;
}

sub sqlquery {
	my $dbh   = shift;
	my $query = shift;
	my @args  = @_;

	my $sth = $dbh->prepare($query) || fatal_api_error(500,"Failed to execute SQL query");
	$sth->execute(@args) || fatal_api_error(500,"Failed to execute SQL query");
	return $sth;
}
