#!/usr/bin/perl
use strict;
use warnings;
use Getopt::Std;
use Date::Parse;
use POSIX qw ( ceil );
use Fcntl ':flock';
use Time::HiRes qw ( time sleep );
use Fcntl;
use Video::Frequencies;
use Video::ivtv 0.13;

my $Tv = "/usr/local/bin/tv";
my $Xine = "/usr/local/bin/xine";
my $MPlay = "/usr/local/bin/mplayer";
my $APlay = "/usr/bin/aplay";
my $Ptune = "/usr/local/bin/ivtv/ptune-ui.pl";
my $IrRC = "/usr/local/bin/irrc";
my $TunerNum = 0;
my %FreqCh = %NTSC_BCAST_JP;
my $Lock = "/var/lock/video/LCK..video";
my $LockBS = $Lock . "BS";
my $Dev = "/dev/video";
my %ChNo = (
	    'NHK'    => 1,
	    'MX'     => 2,
	    'ETV'    => 3,
	    'NTV'    => 4,
	    'nippon' => 4,
	    'TVK'    => 5,
	    'TBS'    => 6,
	    'CX'     => 8,
	    'fuji'   => 8,
	    'tokyu'  => 9,
	    'EX'     => 10,
	    'asahi'  => 10,
	    'UAIR'   => 11,
	    'TX'     => 12,
	    'tokyo'  => 12,
	    );
my $Infinite = "24h";
my $PeerAddr;
my $UDPSA;
my $DevNum;
my $Freq;
my $RecSec;
my %Locked;
my @MonthName = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
		 'Jly', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec');

our ($opt_0, $opt_1, $opt_x, $opt_X, $opt_u, $opt_l,
     $opt_c, $opt_f, $opt_r, $opt_o, $opt_t, $opt_j,
     $opt_v, $opt_P, $opt_U);
getopts('01xXulc:f:r:o:t:jvP:U:') || &help;
my $Verbose = $opt_v;
if ($opt_P) {
    use Socket;
    use Socket6;
    use Carp;
    my $port = $opt_P;
    my $proto = getprotobyname('tcp');
#   socket(SERVER, PF_INET, SOCK_STREAM, $proto) || die "socket: $!";
#   bind(SERVER, sockaddr_in($port, INADDR_ANY)) || die "bind: $!";
    socket(SERVER, AF_INET6, SOCK_STREAM, $proto) || die "socket: $!";
    bind(SERVER, sockaddr_in6($port, pack("NNNN", 0))) || die "bind: $!";
    listen(SERVER, SOMAXCONN) || die "listen: $!";
    exit 0 if fork;
    my $paddr;
    while ($paddr = accept(CLIENT, SERVER)) {
	if (!fork) {
	    close(STDOUT);
	    open(STDOUT, ">&CLIENT");
	    my $request;
	    while (<CLIENT>) {
		$request .= $_;
		last if /^\r/;
	    }
	    print STDERR "--\n${request}--\n" if $Verbose;
	    if ($request =~ /^(GET|POST)\s+\/\?(\S+)/) {
		for (split('&', $2)) {
		    if (/(\w+)=(.*)/) {
			my ($var, $val) = ($1, $2);
			print STDERR "$var=$val\n" if $Verbose;
			if ($var eq "c") {
			    $opt_c = $val;
			} elsif ($var eq "l") {
			    $opt_l = $val;
			}
		    }
		}
	    }
	    $| = 1;
	    print "HTTP/1.1 200 OK\r\nContent-Type: video/mpeg\r\n\r\n";
	    undef $opt_P;
	    if (! $opt_r) {
		$opt_r = $Infinite;
	    }
	    $PeerAddr = getpeername(CLIENT);
	    last;
	}
	wait;
	close(CLIENT);
    }
    close(SERVER);
    exit 1 if $opt_P;
}
if ($opt_U) {
    if ($opt_U =~ /(.*)\:([\w\d]+)$/) {
	my $host = $1;
	my $port = $2;
	use Socket;
	use Socket6;
	use Carp;
	my @res = getaddrinfo($host, $port, AF_UNSPEC, SOCK_DGRAM);
	my $family = -1;
	my $socktype;
	my $proto;
	my $canonname;
	while (scalar(@res) >= 5) {
	    ($family, $socktype, $proto, $UDPSA, $canonname, @res) = @res;
	    if (socket(UDPSOCKET, $family, $socktype, $proto)) {
		print STDERR "send udp to ${host}:$port\n" if $Verbose;
		last;
	    }
	    undef $UDPSA;
	}
    } else {
	print STDERR "illegal dest: $opt_U\n";
	exit 1;
    }
}
my $JustTime;
if ($opt_j) {
    $JustTime = ceil((time+1) / 60) * 60;
}
if ($opt_0) {
    $DevNum = 0;
} elsif ($opt_1) {
    $DevNum = 1;
} else {
    if (($opt_r && ! $opt_t) || $opt_U) {
	my @order;
	if ($opt_r eq $Infinite) {
	    @order = (1, 0);
	} elsif ($opt_r && ($opt_c || $opt_f)) {
	    @order = (0, 1);
	} else {
	    @order = (1, 0);
	}
	undef $DevNum;
	for (my $j=0; $j < 10; $j++) {
	    for my $i (@order) {
		if (! &islocked($Lock . $i)) {
		    $DevNum = $i;
		    last;
		}
	    }
	    last if defined($DevNum);
	    sleep 10;
	}
	defined($DevNum) || die "all ports are used\n";
    } else {
	$DevNum = 1;
    }
}

END {
    for (keys %Locked) {
	if ($_ eq $LockBS) {
	    system $IrRC, "vPower";
	}
	if (-f $_) {
	    unlink $_;
	}
    }
}

if ($opt_c) {
    if ($ChNo{$opt_c}) {
	$opt_c = $ChNo{$opt_c};
    }
    if ($FreqCh{$opt_c}) {
	$Freq = $FreqCh{$opt_c};
    } elsif ($opt_c =~ /^BS\d+$/) {
	if (! $opt_t) {
	    my $bsok = 0;
	    for (my $j=0; $j < 10; $j++) {
		if (! &islocked($LockBS)) {
		    system $IrRC, "vPower";
		    $bsok = 1;
		    last;
		}
		sleep 10;
	    }
	    $bsok || die "BS tuner is used\n";
	    $opt_l = 1;
	    system $IrRC, "v$opt_c";
	    undef $opt_c;
	}
    } else {
	die "Unknown channel '$opt_c'\n";
    }
}
if ($opt_f) {
    $Freq = $opt_f;
}
if ($opt_r) {
    if ($opt_r =~ /^((\d+)h)?((\d+)m)?((\d+)s?)?$/) {
	my $h = $2;
	my $m = $4;
	my $s = $6;
	$h = 0 unless defined $h;
	$m = 0 unless defined $m;
	$s = 0 unless defined $s;
	$RecSec = (($h * 60) + $m) * 60 + $s;
    } else {
	die "Can't parse '$opt_r'\n";
    }
    if ($opt_o) {
	die "File exists: $opt_o\n" if -f $opt_o;
	sysopen(OUTPUT, $opt_o, O_CREAT | O_WRONLY | O_LARGEFILE)
	    || die "Can't create '$opt_o' $!\n";
	print STDERR "recording $RecSec sec to $opt_o\n" if $Verbose;
    } else {
	open(OUTPUT, ">&STDOUT");
    }
}
if ($opt_X) {
    if (!fork) {
	my @mopt = ("-dr", "-quiet", "-fps", "30", "-aspect", "4:3",
		    "-rawvideo", "on:ntsc:hm12", $Dev . ($DevNum + 32));
	my @aopt = ("-q", "-f", "dat", $Dev . ($DevNum + 24));
	if (!$Verbose) {
	    close(STDOUT);
	    open(STDOUT, "/dev/null");
	}
	print STDERR "exec $APlay ", join(" ", @aopt), "\n" if $Verbose;
	my $apid;
	if (!($apid = fork)) {
	    close(STDERR);
	    open(STDERR, "/dev/null");
	    exec $APlay, @aopt;
	}
	print STDERR "exec $MPlay ", join(" ", @mopt), "\n" if $Verbose;
	my $mpid;
	if (!($mpid = fork)) {
	    &dolock("$Lock$DevNum", 'x', 0) || exit 1;
	    close(STDERR);
	    open(STDERR, "/dev/null");
	    exec $MPlay, @mopt;
	}
	waitpid($mpid, 0);
	kill 15, $apid;
	unlink "$Lock$DevNum";
	exit 0;
    }
    sleep 5;
}
if ($opt_x) {
    print STDERR "exec $Xine stdin://mpeg2 < $Dev$DevNum\n" if $Verbose;
    if (!fork) {
	&dolock("$Lock$DevNum", 'x', 0) || exit 1;
	close(STDIN);
	open(STDIN, "$Dev$DevNum");
	exec $Xine, 'stdin://mpeg2';
    }
    sleep 5;
}
if ($opt_u) {
    my @opts = ('-d', "$Dev$DevNum", '--freqtable', 'ntsc-bcast-jp');
    print STDERR "exec $Ptune ", join(" ", @opts), "\n" if $Verbose;
    if (!fork) {
	exec $Ptune, @opts;
    }
}
if ($opt_t) {
    my $time = str2time($opt_t);
    my $now = time;
    $time += (60*60)*24 if $time < $now && $now < $time + (60*60)*24;
    my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)
	= localtime($time);
    die "sec must be 0 at `-t <time>'\n" if $sec > 0;
    printf(STDERR "%d/%d %02d:%02d - ", $mon+1, $mday, $hour, $min);
    ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)
	= localtime($time + $RecSec);
    printf(STDERR "%d/%d %02d:%02d\n", $mon+1, $mday, $hour, $min);
    ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)
	= localtime($time - 59);
    die "specify `-r <sec>' with -t" if ! $RecSec;
    die "specify `-o <file>' with -t" if ! $opt_o;
    close(OUTPUT);
    my $atcom = sprintf("at %02d:%02d %s %d",
			$hour, $min, $MonthName[$mon], $mday);
    my $comm = "$Tv -j -r $RecSec -o \"$opt_o\"";
    my $i = 0;
    if ($opt_c) {
	$comm .= " -c $opt_c";
	$i++;
    }
    if ($opt_f) {
	$comm .= " -f $opt_f";
	$i++;
    }
    if ($opt_l) {
	$comm .= " -l";
	$i++;
    }
    $i == 1 || die "specify one of `-c <channel>', `-f <freq>' or `-l'";
    print STDERR "$atcom\n$comm\n" if $Verbose;
    open(CRON, "| $atcom") || die "failed to exec $atcom";
    print CRON "sleep 10\n";
    print CRON "rm -f \"$opt_o\"\n";
    print CRON "$comm\n";
    close(CRON);
    if ($? << 8) {
	die "failed to exec $atcom";
    }
    exit 0;
}

open(TUNER, "<$Dev$DevNum") || die "Error opening $Dev$DevNum ! $!";
my $TunerFD = fileno(TUNER);
my $IvTV = Video::ivtv->new();

#$done = 0;
#for (my $i=0; !$done; $i++) {
#    my ($index, $id, $name,
#	$frameperiod_numerator, $frameperiod_denominator, $framelines)
#	= $IvTV->enumerateStandard($TunerFD, $i);
#    if ($index < 0) {
#	$done = 1;
#    } else {
#	print STDERR $index, ', ', $id, ': "', $name, '", ',
#		$frameperiod_denominator, ', ', $framelines, "\n" if $Verbose;
#	$IdStandard{$name} = $id;
#	$NameNTSC = $name
#	    if (!defined $NameNTSC && $name =~ /NTSC/) || ($name =~ /NTSC.*J/);
#    }
#}
#if (defined $IdStandard{$NameNTSC}) {
#    print STDERR "change standard to '$NameNTSC'\n" if $Verbose;
#    $IvTV->setStandard($TunerFD, $IdStandard{$NameNTSC})
#	|| die "setStandard($IdStandard{$NameNTSC}) failed\n";
#} else {
#    die "Can't change standard to '$NameNTSC'\n";
#}
my %NameInput;
my $NameVideo;
my $NameTuner;
my $Input;
my $done = 0;
for (my $i=0; !$done; $i++) {
    my ($index, $name, $type, $audioset, $tuner, $std, $status)
	= $IvTV->enumerateInput($TunerFD, $i);
    if ($index < 0) {
	$done = 1;
    } else {
	print STDERR $tuner, ', ', $index, ': ', $name, "\n" if $Verbose;
	$NameInput{$name} = $index;
	$NameVideo = $name if $name =~ /S-Video/i;
	$NameTuner = $name if $name =~ /Tuner/i;
    }
}
print "Video: '$NameVideo', Tuner: '$NameTuner'\n" if $Verbose;
if ($opt_l) {
    $Input = $NameVideo;
} else {
    $Input = $NameTuner;
}
if (defined $NameInput{$Input}) {
    print STDERR "change input to '$Input'\n" if $Verbose;
    $IvTV->setInput($TunerFD, $NameInput{$Input})
	|| die "setInput($NameInput{$Input}) failed\n";
} else {
    die "Can't change input to '$Input'\n";
}
if ($Freq) {
    print STDERR "change freq to '$Freq'\n" if $Verbose;
    &setFreq;
} else {
    my $f = $IvTV->getFrequency($TunerFD, $TunerNum);
    $Freq = ($f * 1000)/16;
    print STDERR "current freq is '$Freq'\n" if $Verbose;
}
my $Signal;
my $StopRec;
if ($RecSec || $UDPSA) {
    $SIG{PIPE} = \&handler;
    $SIG{ALRM} = \&handler;
    $SIG{INT} = \&handler;
    my $stoptime;
    if ($RecSec) {
	if ($JustTime) {
	    $stoptime = $JustTime + $RecSec + 0.25;
	    my $sleep = $JustTime - time - 0.03;
	    if ($sleep > 0) {
		print STDERR "sleep $sleep sec\n" if $Verbose;
		sleep $sleep;
	    }
	} else {
	    $stoptime = time + $RecSec;
	}
	alarm($RecSec + 60);
    } elsif ($UDPSA) {
	my $now = time;
	$stoptime = $now + 24*60*60;
    }
    $done = 0;
    my $endStream = 0;
    while (!$done) {
	my $now = time;
	if ($StopRec || $now > $stoptime) {
	    my $stopflag = 1;
	    if ($Signal) {
		print STDERR "Caught a SIG$Signal\n" if $Verbose;
		if ($Signal eq 'ALRM' && $now < $stoptime) {
		    print STDERR "Premature ALRM, ignore" if $Verbose;
		    alarm(ceil($stoptime - $now + 1));
		    $stopflag = 0;
		} elsif ($Signal eq 'PIPE' && $PeerAddr) {
		    print STDERR "Peer closed\n" if $Verbose;
		} elsif ($StopRec) {
		    print STDERR "Interrupted by SIG$Signal, ",
			($stoptime - $now), " sec left\n";
		}
		undef $Signal;
	    }
	    if ($stopflag) {
		$IvTV->stopEncoding($TunerFD);
		$endStream = 1;
		$done = 1;
	    }
	    $StopRec = 0;
	}
	# read from the device and write to the file.  (16384) 32768
	my $buf;
	my $bytes = read(TUNER, $buf, 32768);
	last if (!$bytes && $endStream);
	if ($UDPSA) {
	    send(UDPSOCKET, $buf, 0, $UDPSA);
	}
	if ($RecSec) {
	    print OUTPUT $buf;
	}
    }
    close(OUTPUT);
}
close(TUNER);
exit 0;

sub setFreq {
    if ($Freq) {
	$IvTV->setFrequency($TunerFD, $TunerNum, ($Freq * 16)/1000)
	    || die "setFrequency($Freq) failed\n";
	print STDERR "setFreq $Freq\n" if $Verbose;
    }
}

sub islocked {
    my ($file) = @_;
    my $found = 0;
    my $pid = 0;
    if (open(LOCK, $file)) {
	$_ = <LOCK>;
	close(LOCK);
	if (/^\s*(\d+)(.*)/) {
	    $pid = $1;
	    my $flag = $2;
	    if (open(PS, "ps ax |")) {
		while (<PS>) {
		    if (/^\s*$pid\s/) {
			print STDERR "lock file exists: $file\n" if $Verbose;
			if ($flag = /x/ && /\s$Xine\s/) {
			    print STDERR "kill xine that created $file\n"
				if $Verbose;
			    kill 15, $pid;
			} else {
			    $found = 1;
			}
		    }
		}
		close(PS);
	    }
	    if (! $found) {
		print STDERR "ignore old lock file: $file\n" if $Verbose;
	    }
	}
    }
    if (! $found) {
	&dolock($file, "", $pid) && return 0;
    }
    1;	# locked
}

sub dolock {
    my ($file, $flag, $ignore) = @_;
    my $ret = 0;
    my $umask = umask 0002;
    print STDERR "lock $file, $flag\n" if $Verbose;
    if (open(LOCK, "+>>$file")) {
	if (flock(LOCK, LOCK_EX)) {
	    seek(LOCK, 0, 0);
	    $_ = <LOCK>;
	    if (!defined $_ || $_ !~ /^\s*(\d+)/ || $1 == $ignore) {
		$ret = 1;
		seek(LOCK, 0, 0);
		truncate(LOCK, 0);
		print LOCK "$$ $flag\n";
	    } else {
		print STDERR "locked by $1\n" if $Verbose;
	    }
	    flock(LOCK, LOCK_UN);
	}
	close(LOCK);
    }
    umask $umask;
    if ($ret) {
	$Locked{$file} = 1;
    } else {
	delete $Locked{$file};
    }
    $ret;
}

sub handler {
    ($Signal) = @_;
    $StopRec = 1;
}

sub help {
    print STDERR <<EOF;
Usage: tv <opt>
opt:   -x               ; xine
       -X               ; mplayer YUV
       -u               ; ptune-ui
       -P <port>        ; TV server (http)
       -U <host>:<port> ;           (udp)
       -c <channel>     ; change channel
       -f <freq>        ; chage frequency
       -l               ; input from S-Video
       -r <sec>         ; mpeg to stdout
       -o <file>        ;      to file
       -t <time>        ; record at <time>
       -j               ; start at 0 sec
       -0               ; use video0
       -1               ; use video1
       -v               ; verbose
EOF
    exit 1;
}
