#! /usr/bin/perl
#
# Informer daemon for Vargus
###############################
#    Copyright (C) 2010-2012 Michael A. Kangin <mak@complife.ru>
#
#    Copyright: Vargus is under GNU GPL, the GNU General Public License.
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; version 2 dated June, 1991.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    http://www.gnu.org/licenses/gpl-2.0.html
#


use strict;
use warnings;

use Getopt::Long;
use IO::Socket;
use Proc::Daemon;
use Privileges::Drop;
use Sys::Syslog qw(LOG_DAEMON LOG_ERR LOG_WARNING LOG_NOTICE LOG_INFO LOG_DEBUG);
use Net::Domain qw(hostdomain);
use URI::URL;


use Vargus::Common;
use Vargus::Objects;

our $conf_dir = "/etc/vargus";
our $stat_dir = "/var/cache/vargus";
my $config_file = $conf_dir . "/informer.cfg";

our %vargus_objects;
our $obj_name;
our $obj_id;
our $daemon_mode;

our $informer_port = 9165;
my $vargus_user = 'vargus';
my $bind_address = '0.0.0.0';
my $daemon_pid_file = '/var/run/vargus/_informer.pid';
my $slave_servers;
my $master_servers;
my $use_fqdn;
my $use_ip;

my $do_expand_macroses;
my $query_object_type;
my @query_object_property;
my @query_object_name;

my %virtual_cameras;

sub prepare_to_die {
	die;
}

sub configure {
        my @cfg_body = ();
        my @main_section = ();
	my $is_option;

	unless ( -e $config_file ) {
		s_log(LOG_NOTICE, "No config file found, continue with defaults");
		return;
	}

	open(CFG, $config_file) or log_n_die("Error reading config file $config_file");
	chomp(@cfg_body = <CFG>);
	close(CFG);

	@main_section = expand_macroses(get_cfg_section("main", @cfg_body));

	$is_option = get_option("user", @main_section) and
		$vargus_user = $is_option;

	$is_option = get_option("port", @main_section) and
		$informer_port = $is_option;

	$is_option = get_option("bind-address", @main_section) and
		$bind_address = $is_option;

	$is_option = get_option("slave-servers", @main_section) and
		$slave_servers = $is_option;

	$is_option = get_option("master-servers", @main_section) and
		$master_servers = $is_option;

	$is_option = get_option("use-fqdn", @main_section) and
		$use_fqdn = $is_option;

	$is_option = get_option("use-ip", @main_section) and
		$use_ip = $is_option;
}


sub query_slave_servers {
	return unless $slave_servers;
	%virtual_cameras = ();
	my $v_counter = $#{$vargus_objects{camera}} + 2;
	foreach my $server (split(',', $slave_servers)) {
		my $cams_by_server = do_remote_query($server, 'query camera;quantity');
		if ($cams_by_server) {
			my $cam_query = 'query camera;1';
			foreach my $cam_no (2..$cams_by_server) {
				$cam_query .= "," . $cam_no;
			}
			$cam_query .= ';name';
			my $cams_info = do_remote_query($server, $cam_query);
			foreach my $rcam_name (split(/\n/, $cams_info)) {
				$virtual_cameras{$rcam_name}{num} = $v_counter++;
				$virtual_cameras{$rcam_name}{server} = $server;
			}
		}
	}
}



sub get_obj_num_by_name {
	my $object_type = shift;
	my $object_name = shift;

	my @objects = @{$vargus_objects{$object_type}};
	my $index = 0;
	foreach (@objects) {
		last if $objects[$index]{name} eq $object_name;
		$index++;
	}
	my $_404_not_found = $#objects < $index;

	return $index + 1 unless $_404_not_found;

	if ($_404_not_found && $object_type eq 'camera') {
		return $virtual_cameras{$object_name}{num} if $virtual_cameras{$object_name};
		query_slave_servers;
		return $virtual_cameras{$object_name}{num} if $virtual_cameras{$object_name};
	}

	s_log(LOG_WARNING, "No object with type '$object_type' and name '$object_name'");
	return -1;
}


sub get_obj_info {
	my $object_type = shift;
	my $object_num = shift;
	my $what_info = shift;

	my $object = "\${\$vargus_objects{$object_type}}[$object_num - 1]";
	
	$obj_name = eval("$object" . "{name}");
	$obj_id = eval("$object" . "{id}");

	my $query = "$object$what_info";
	my $answer = eval($query);
	$answer or return;
	$answer =~ s/\(get_port:/(find_port:/;
	$answer = expand_macroses($answer) if $do_expand_macroses;

	if (($use_fqdn || $use_ip) && $what_info =~ /\{view\}\{(source|preview|farview)\}/) {
		my $url = URI::URL->new($answer);
		if ($use_fqdn) {
			my $domainname = hostdomain();
			$url->host($url->host() . ".$domainname") unless $url->host() =~ /$domainname$/;
		}
		if ($use_ip) {
			my $ip = join('.', unpack('C4', gethostbyname($url->host())));
			$url->host($ip);
		}
		$answer = $url->as_string;
	}

	return "$answer";
}


sub process_query {
	my @answers = ();
	my %server_queries = ();

	$query_object_type or log_n_die "No query type specified";
	$query_object_type eq 'camera' 
		or $query_object_type eq 'set' 
		or $query_object_type eq 'view' 
		or log_n_die "Unknown query_object_type $query_object_type";

	@query_object_property or log_n_die "No query specified";
	@query_object_property = split(/,/,join(',',@query_object_property));

	if ($query_object_property[0] eq 'quantity') {
		my @objects = @{$vargus_objects{$query_object_type}};
		my $q_objects = $#objects + 1;
		if ($query_object_type eq 'camera') {
			query_slave_servers if not %virtual_cameras;
			$q_objects += scalar keys %virtual_cameras;
		}
		push(@answers, $q_objects);
		shift(@query_object_property);
	}

	@query_object_name or log_n_die "No object name specified" if @query_object_property;
	@query_object_name = split(/,/,join(',',@query_object_name));

	query_slave_servers if not %virtual_cameras and $query_object_type eq 'camera';

	for (my $names_counter = 0; $names_counter <= $#query_object_name; $names_counter++) {
		my $answer = '';
		my $name = $query_object_name[$names_counter];
		my $object_num;
		$name or next;

		if ($name =~ /[^0-9]/) {
			$object_num = get_obj_num_by_name($query_object_type, $name);
		} else {
			$object_num = $name;
		}

		next if $object_num == -1;

		my @objects = @{$vargus_objects{$query_object_type}};
		if ($#objects  < $object_num - 1) {
			my $is_virtual_camera = 0;
			if ($query_object_type eq 'camera') {
				foreach my $cam_name (keys %virtual_cameras) {
					next unless $virtual_cameras{$cam_name}{num} == $object_num;
					$server_queries{$virtual_cameras{$cam_name}{server}}{$names_counter} = $cam_name;
					$is_virtual_camera = 1;
					last;
				}
			}
			log_n_die "Wrong index '$name' for '$query_object_type' objects" unless $is_virtual_camera;
		} else {
			foreach my $query_property (@query_object_property) {
				my $property;
				$property = $query_property;
				$property =~ /[^a-zA-Z0-9:]/ and s_log(LOG_WARNING, "Warning! Property $property may be not correct");
				$property =~ s/:/}{/g;
				$property =~ s/=/}[/g;
				$property = "{$property}";
				$property =~ s/(\[[0-9]+)}/$1]/g;

				my $this_info = get_obj_info($query_object_type, $object_num, $property);
				$answer .= ($this_info ? $this_info : "")  . ";";
			}
			$answer =~ s/;$//g;
			$answer =~ s/,$//g;
			$answers[$names_counter] = $answer;
		}
	}

	foreach my $server (keys %server_queries) {
		my $answer = '';
		my @answer_positions = ();
		my $r_query = 'query ';
		$r_query .= '!expand ' unless $do_expand_macroses;
		$r_query .= "camera;";
		foreach my $names_position (keys %{$server_queries{$server}}) {
			$r_query .= $server_queries{$server}{$names_position} . ",";
			push(@answer_positions, $names_position);
		}
		$r_query =~ s/,$//;
		$r_query .= ';' . join(',', @query_object_property);

		$answer = do_remote_query($server, $r_query);
		chomp($answer);

		my $answer_position = 0;
		foreach my $remote_answer (split(/\n/, $answer)) {
			$answers[$answer_positions[$answer_position++]] = $remote_answer;
		}
	}

	return join("\n", @answers) . "\n";
}


sub info_server_help {
	my $help = <<END;
	Usage:

	query TYPE;NAME-LIST;PROPERTY-LIST - get list of propertyes for list of names for object-type.
	TYPE can be: camera, view, set.
	NAME: number or name of object. Multiple NAMEs divided by commas.
	PROPERTY: multiple propertyes divided by commas.
	Example: query camera;cam1,cam2;view:preview,view:farview

	findport PORTNAME - find port with PORTNAME name;

	update-remotes - query cameras list from slave servers;

	exit - exit.
END
	return $help;
}



sub net_daemon {

	die "Pid file already exist" if -e $daemon_pid_file;

	Proc::Daemon::Init;

	$SIG{INT} = $SIG{TERM} = $SIG{PIPE} = sub {
		if (-e $daemon_pid_file) {
			open(PIDFILE, $daemon_pid_file) or die;
			my ($test_pid) = <PIDFILE>;
			close(PIDFILE);
			unlink $daemon_pid_file if $test_pid == $$;
		}
		die;
	};

	$SIG{CHLD} = 'IGNORE';
	$SIG{USR1} = sub {
		query_slave_servers;
	};

	open(PIDFILE, "> $daemon_pid_file") or s_log(LOG_WARNING, "Can't create pid file $daemon_pid_file");
	print PIDFILE $$;
	close(PIDFILE);

	my $lsocket = IO::Socket::INET -> new(
		LocalPort => $informer_port,
		Listen => 5,
		Proto => 'tcp',
		Bind => $bind_address,
		ReuseAddr => 1,
		Timeout => 5,
	) or log_n_die "Can't create socket";

	$SIG{ALRM} = sub {
		$SIG{ALRM} = 'IGNORE';
		foreach my $server (split(',', $master_servers)) {
			my $answer = do_remote_query($server, 'update-remotes');
		}
	};

	alarm(1);

	while (1) {
		next unless my $connection = $lsocket->accept;
		defined (my $pid = fork()) or log_n_die "Can't fork new child: $!";
		$SIG{ALRM} = 'IGNORE';
		unless ($pid) {
			$lsocket->close;
			serve_client($connection);
			exit 0;
		}
		$connection->close;
	}
}

sub serve_client {
	my $sock = shift;

	print $sock ": Vagrus informer. Enter your query or \"help\" for help.\n";
	print $sock ">\n";

	while (<$sock>) {
		chomp;
		s/\r*$//;
		$_ or next; 
		s_log(LOG_DEBUG, "Get query: $_");
		if (/^(exit|quit)$/) {
			print $sock ": Bye.\n";
			last;
		} elsif (/^help$/) {
			print $sock info_server_help;
		} elsif (/^update-remotes$/) {
			my $p_pid = getppid();
			kill (10, $p_pid);
			last;
		} elsif (/^query($|\s)/) {
			my (undef,$query_string) = split(' ', $_, 2);
			$do_expand_macroses = $query_string =~ s/^!expand //;
			$do_expand_macroses = !$do_expand_macroses;
			my ($q_type, $q_name, $q_prop) = split(';', $query_string);
			unless ($query_object_type = $q_type) {
				print $sock ": No TYPE info. See 'help' for details\n";
				next;
			}

			unless ($query_object_type =~ /camera|view|set/) {
				print $sock ": Wrong query TYPE. See 'help' for details\n";
				next;
			}

			if ($q_name eq 'quantity' and ! $q_prop) {
				$q_prop = $q_name;
				$q_name = '';
			}

			@query_object_name = split(',', $q_name);
			@query_object_property = split(',', $q_prop);
			
			unless (@query_object_property) {
				print $sock ": No PROPERTY for query. See 'help' for details\n";
				next;
			}
			print $sock process_query;

		} elsif (/^findport($|\s)/) {
			my (undef, $port) = split(' ', $_);
			unless ($port) {
				print $sock ": Bad value $port for findport command\n";
				next;
			}
			print $sock expand_macroses("(find_port:$port)") . "\n";
		} else {
			print $sock ": Unknown command. See 'help' for details\n";
		}
	}
	continue {
		print $sock ">\n";
	}
}


configure;
my ($new_uid, $new_gid, @new_gids) = drop_privileges($vargus_user) or log_n_die("Could not drop privileges") if not $>;

$obj_name = "";
$obj_id = "";

init_objects;
query_slave_servers;

$do_expand_macroses = 1;

GetOptions(     't=s' => \$query_object_type,
                'n=s' => \@query_object_name,
                'q=s' => \@query_object_property,
		'expand!' => \$do_expand_macroses,
		'daemon!' => \$daemon_mode
);

net_daemon if $daemon_mode;

print(process_query);
