# Copyright (C) 2020 Quentin Sculo <squentin@free.fr>
#
# This file is part of Gmusicbrowser.
# Gmusicbrowser is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3, as
# published by the Free Software Foundation

package Tag::Generic::GStreamer;

use strict;
use warnings;

our $OK;

our %GenericTag= # keys use vorbis comment name standard for simplicity
(	title	=> 'title',
	album	=> 'album',
	artist	=> 'artist',
	date	=> 'datetime',
	genre	=> 'genre',
	version	=> 'version',
	composer=> 'composer',
	comment	=> 'comment',
	description => 'description',
	album_artist=> 'album-artist',
	tracknumber => 'track-number',
	discnumber  => 'album-disc-number',
	conductor   => 'conductor',
);

# used to try to clean up some of the format names a bit, some simplifications may be bad
our %Formats=reverse
(	#containers
	ASF =>		'Advanced Streaming Format (ASF)',
	AVI =>		'Audio Video Interleave (AVI)',
	CDXA =>		'RIFF/CDXA (VCD)',
	Flash=>		'Flash',
	WebM =>		'WebM',
	Matroska =>	'Matroska',
	Ogg =>		'Ogg',
	Quicktime =>	'Quicktime',
	Realmedia =>	'Realmedia',
	'MPEG-1 PS' =>	'MPEG-1 System Stream',
	'MPEG-2 PS' =>	'MPEG-2 System Stream',
	'MPEG-2 TS' =>	'MPEG-2 Transport Stream',
	#audio
	'AC-3'=>	'AC-3 (ATSC A/52)',
	'AC-3'=>	'DVD AC-3 (ATSC A/52)',
	'AC-3'=>	'E-AC-3 (ATSC A/52B)',
	ADCPM =>	'DVI ADPCM',
	ADCPM =>	'Quicktime ADPCM',
	ADCPM =>	'Microsoft ADPCM',
	ADCPM =>	'A-Law',
	AMR =>		'Adaptive Multi Rate (AMR)',
	MP1 =>		'MPEG-1 Layer 1 (MP1)',
	MP2 =>		'MPEG-1 Layer 2 (MP2)',
	MP3 =>		'MPEG-1 Layer 3 (MP3)',
	PCM =>		'Raw 16-bit PCM audio',
	PCM =>		'Raw 8-bit PCM audio',
	WAV =>		'WAV',
	DTS =>		'DTS',
	AAC =>		'MPEG-4 AAC',
	AAC =>		'MPEG-2 AAC',
	Vorbis =>	'Vorbis',
	Opus =>		'Opus',
	FLAC =>		'Free Lossless Audio Codec (FLAC)',
	ALAC =>		'Apple Lossless Audio (ALAC)',
	QDM2 =>		'QDesign Music (QDM) 2',
	Cook =>		'RealAudio G2 (Cook)',
	Voxware =>	'Voxware',
	GSM =>		'MS GSM',
	MOD =>		'Module Music Format (MOD)',
	MIDI=>		'audio/midi',
	WMA1 =>		'Windows Media Audio 7',
	WMA2 =>		'Windows Media Audio 8',
	WMA3 =>		'Windows Media Audio 9',
	'WMA Voice' =>	'Windows Media Speech',
	#video
	'Sorenson 1' =>	'Sorensen Video 1',
	'Sorenson 3' =>	'Sorensen Video 3',
	'Sorenson Spark' => 'Sorenson Spark Video',
	'RealVideo 4' =>'RealVideo 4.0',
	'MPEG-1' =>	'MPEG-1 Video',
	'MPEG-2' =>	'MPEG-2 Video',
	'MPEG-4' =>	'MPEG-4 Video',
	'MS MPEG-4 4.1' => 'Microsoft MPEG-4 4.1',
	'MS MPEG-4 4.2' => 'Microsoft MPEG-4 4.2',
	'MS MPEG-4 4.3' => 'Microsoft MPEG-4 4.3',
	'MS-CRAM'=>	'Microsoft Video 1',
	'H.265' =>	'H.265',
	'H.264' =>	'H.264',
	'H.264' =>	'ITU H.264',
	'H.26n' =>	'ITU H.26n',
	'DivX 3' =>	'DivX MPEG-4 Version 3',
	'DivX 4' =>	'DivX MPEG-4 Version 4',
	'DivX 5' =>	'DivX MPEG-4 Version 5',
	'DivX 3' =>	'DivX 3',
	'DivX 4' =>	'DivX 4',
	'DivX 5' =>	'DivX 5',
	Cinepak	=>	'Cinepak Video',
	VP6 =>		'On2 VP6/Flash',
	VP8 =>		'VP8',
	VP9 =>		'VP9',
	WMV1 => 	'Windows Media Video 7',
	WMV1 => 	'Windows Media Video 7 Screen',
	WMV2 =>		'Windows Media Video 8 Screen',
	WMV3 =>		'Windows Media Video 9 Screen',
	JPEG =>		'JPEG',
	'Indeo 3' =>	'Intel Indeo 3',
	'Indeo 4' =>	'Intel Indeo 4',
	'Indeo 5' =>	'Intel Indeo 5',
	I263 =>		'Intel H.263',
);

if (caller)
{	# loaded from gmb
	$OK= system('env','perl',__FILE__) ? 0 : 1; # launch it without argument to test init()
	warn "Error trying to initialize gstreamer generic metadata reader: won't be able to use it to add partially supported files\n" unless $OK;
}
else	# independent process: scan file passed as arg
{	die unless init();
	my $uri= shift;  # when called by gmb it should already be a valid uri
	exit 0 unless defined $uri;
	unless ($uri=~m#^\w+://#) # convert non-uri to uri so that it can also be called from command line
	{	$uri=$ENV{PWD}.'/'.$uri unless $uri=~m#^/#;
		$uri=~s#([^/\$_.+!*'(),A-Za-z0-9-])#sprintf('%%%02X',ord($1))#seg;
		$uri="file://$uri";
	}
	my $self= bless {};
	$self->discover($uri);
	$self->print_yaml_result;
	close STDERR; # to get rid of warnings on exit
}


sub init
{	use Glib::Object::Introspection;
	Glib::Object::Introspection->setup(basename => 'Gst', version => '1.0', package => 'GStreamer1');
	GStreamer1::init_check([ $0, @ARGV ]) or die "Can't initialize gstreamer-1.x\n";
	Glib::Object::Introspection->setup(basename => 'GstPbutils', version => '1.0', package => 'GStreamer1::Pbutils');
	#Glib::Object::Introspection->setup(basename => 'GstTag', version => '1.0', package => 'GStreamer1::Tag'); # could be useful for something in the future
	#Glib::Object::Introspection->setup(basename => 'GLib', version => '2.0', package => 'GLib'); # maybe needed to use the GDate type but can't get it to work, maybe it conflicts with some glib parts of Glib::Object::Introspection ?
	return 1;
}

sub new
{	my ($class,$file)=@_;
	my $self=bless {}, $class;

	$self->{filename} = $file;
	my $uri=$file;
	unless ($uri=~m#^\w+://#)
	{	unless (-e $file)
		{	warn "File '$file' does not exist.\n";
			return undef;
		}
		$uri= "file://".::url_escape($uri);
	}

	if (1) { $self->launch_and_parse($uri); }# do it in another process (safer)
	else { $self->discover($uri); }		 # do it in same process (cause warnings and segfault on exit, and could maybe crash gmb)

	return undef unless $self->{info};
	return $self;
}

sub launch_and_parse
{	my ($self,$uri)=@_;
	my @cmd_and_args= ('env','perl',__FILE__,$uri);
	pipe my($content_fh),my$wfh;
	my $pid=fork;
	if (!defined $pid) { warn "fork failed : $!\n"; }
	elsif ($pid==0) #child
	{	close $content_fh; #close $error_fh;
		open \*STDOUT,'>&='.fileno $wfh;
		close STDERR;
		exec @cmd_and_args  or warn "launch failed (@cmd_and_args)  : $!\n";
		POSIX::_exit(1);
	}
	close $wfh;
	my @output;
	binmode $content_fh,':utf8';
	while (<$content_fh>)
	{	push @output, $_;
	}
	close $content_fh;
	::ReadRefFromLines(\@output,$self);
}

sub discover
{	my ($self,$uri)=@_;
	my $discoverer= GStreamer1::Pbutils::Discoverer->new(GStreamer1::SECOND() * 5) or die;
	$discoverer->signal_connect(discovered => \&on_discovered_cb,$self);
	my $info=$discoverer->discover_uri($uri);
	on_discovered_cb($discoverer,$info,undef,$self);
}

# print out results in YAML format. To simplify, each value of info and tag can only be a string or an array of strings
sub print_yaml_result
{	my $self=shift;
	binmode STDOUT,':utf8';
	for my $info_tag (qw/info tag/)
	{	my $hash= $self->{$info_tag};
		next unless $hash;
		print "$info_tag:\n";
		for my $key (sort keys %$hash)
		{	my $lines= "  $key:";
			my $val= $hash->{$key};
			next unless defined $val;
			if (ref $val)
			{	next unless ref $val eq 'ARRAY'; #only supports arrays
				next unless @$val;
				$lines.= "\n";
				$lines.= "    - ".yaml_escape($_)."\n" for @$val;
			}
			else
			{	$lines.= " ".yaml_escape($val)."\n"
			}
			print $lines;
		}
	}
}
sub yaml_escape
{	my $val=shift;
	if (!defined $val) {$val='~'}
	elsif ($val eq '') {$val="''"}
	elsif ($val=~m/[\x00-\x1f\n:#]/ || $val=~m#^'#)
	{	$val=~s/([\x00-\x1f\n"\\])/sprintf "\\x%02x",ord $1/ge;
		$val=qq/"$val"/;
	}
	elsif ($val=~m/^\W/ || $val=~m/\s$/ || $val=~m/^true$|^false$|^null$/i)
	{	$val=~s/'/''/g;
		$val="'$val'";
	}
	return $val;
}

sub on_discovered_cb
{	my ($discoverer,$info,$error,$self)=@_;
	my $result= $info->get_result;
	#warn $result." ". $info->get_uri."\n";
	if ($result ne 'ok')
	{	#if ($result eq 'uri_invalid') {}
		#elsif ($result eq 'error') {}
		#elsif ($result eq 'timeout') {}
		#elsif ($result eq 'busy') {}
		#elsif ($result eq 'missing_plugins') {}
		warn "Error '$result' discovering $info->get_uri\n";
		return;
	}
	#warn $info->get_uri;
	#$info->get_seekable
	#$info->get_live
	$self->{info}= {};
	$self->{info}{seconds}= $info->get_duration / GStreamer1::SECOND();
	if (my $tags=$info->get_tags)
	{	my %used= reverse %GenericTag;
		$tags->foreach(
		sub {	my ($tags,$key)= @_;
			return 1 unless exists $used{$key}; #skip fields that are not in values of %GenericTag, as those won't be used anyway and some non-string types could cause crashes like "date"
			my $val;
			if ($key eq 'date') {  }	# GDate type, can't get to it, cause crashes, datetime seems more popular anyway
			elsif ($key eq 'datetime')	# GstDateTime type
			{	eval { $val= $tags->get_date_time($key)->to_iso8601_string; };
			}
			else { eval { $val= $tags->copy_value($key); }; }
			$self->{tag}{$key}=$val if defined $val;
			1;
		});
	}
	$self->{info}{seekable}= $info->get_seekable;
	if (my $gstinfo= $info->get_stream_info)
	{	$self->scan_topology($gstinfo);
		for my $cat (qw/container_format video_format audio_format/)
		{	my $format= $self->{info}{$cat};
			next unless $format;
			# clean-up format string
			$format=~s/ \([-a-z0-9 ]+ Profile\)$//i;
			$self->{info}{$cat}= $Formats{$format} || $format;
		}
	}
}

sub scan_topology
{	my ($self,$gstinfo)=@_;
	my $info= $self->{info};
	my $caps= $gstinfo->get_caps;
	my $type= $gstinfo->get_stream_type_nick;
	my $desc= $caps->is_fixed ? GStreamer1::Pbutils::pb_utils_get_codec_description($caps) : $caps->to_string;
	$info->{$type."_format"} ||= $desc; # only store the desc for the first of a type of stream
	if (my $toc= !$info->{toc} && $gstinfo->get_toc)
	{	my @entries_todo= @{ $toc->get_entries };
		while (my $entry= shift @entries_todo)
		{	my ($start,$stop)= $entry->get_start_stop_times;
			#warn " toc ".$entry->get_entry_type." $start-$stop\n";		#DEBUG
			#my $tags= $entry->get_tags;					#DEBUG
			#$tags->foreach(sub {my $gvalue=$_[0]->copy_value($_[1]);warn "  ".$_[1]." = ".$gvalue."\n"; 1; }) if $tags;#DEBUG
			my $subentries= $entry->get_sub_entries;
			push @entries_todo, @$subentries if $subentries;
			if ($entry->get_entry_type eq 'chapter')
			{	my $tags= $entry->get_tags;
				my $title= $tags->get_string('title');
				push @{ $info->{toc} }, $stop,$title; #seems $start is always 1 ? so just use $stop
			}
		}
	}
	if ($gstinfo->isa('GStreamer1::Pbutils::DiscovererAudioInfo'))
	{	unless ($info->{audio_count}) #only take info from first audio stream
		{	$info->{channels}=	$gstinfo->get_channels;
			$info->{bitrate}=	$gstinfo->get_bitrate;
			$info->{rate}=		$gstinfo->get_sample_rate;
			$info->{audio_depth}=	$gstinfo->get_depth;
		}
		my $lang= $gstinfo->get_language;
		push @{ $info->{audio_lang} }, $lang if $lang;
		$info->{audio_count}++;
	}
	elsif ($gstinfo->isa('GStreamer1::Pbutils::DiscovererVideoInfo') && !$gstinfo->is_image)
	{	unless ($info->{video_count}) #only take info from first video stream
		{	$info->{video_bitrate}=	$gstinfo->get_bitrate;
			$info->{video_depth}=	$gstinfo->get_depth;
			$info->{video_height}=$gstinfo->get_height;
			$info->{video_width}= $gstinfo->get_width;
			$info->{framerate}= $gstinfo->get_framerate_num / $gstinfo->get_framerate_denom;
			$info->{video_par}= $gstinfo->get_par_num / $gstinfo->get_par_denom; #pixel aspect ratio
			$info->{video_ratio}= $info->{video_par} * $info->{video_width} / $info->{video_height};
			$info->{interlaced}= $gstinfo->is_interlaced ? 1 : 0;
		}
		$info->{video_count}++;
	}
	elsif ($gstinfo->isa('GStreamer1::Pbutils::DiscovererSubtitleInfo'))
	{	$info->{subtitle_count}++;
		my $lang= $gstinfo->get_language;
		push @{ $info->{subtitle_lang} }, $lang if $lang;
	}

	my $next= $gstinfo->get_next;
	if ($next)
	{	$self->scan_topology($next);
	}
	elsif ($gstinfo->isa('GStreamer1::Pbutils::DiscovererContainerInfo'))
	{	for my $stream (@{$gstinfo->get_streams})
		{	$self->scan_topology($stream);
		}
	}
}

sub get_values
{	my ($self,$field)=@_;
	my $name= $GenericTag{$field};
	$name ? $self->{tag}{$name} : undef;
}

1;