# Copyright (C) Innuos
# SPDX-License-Identifier: GPL-2.0

package Plugins::TidalConnectLink::ProtocolHandler;

# Handler for tconnectlink:// URLs

use strict;
use base qw(Slim::Player::Protocols::HTTPS);
use Scalar::Util qw(blessed);

use Slim::Utils::Log;
use Slim::Utils::Misc;
use Slim::Utils::Prefs;

use Slim::Formats::RemoteMetadata;
use Slim::Music::Info;
use Slim::Utils::Errno;
use Slim::Utils::Scanner::Remote;
use Slim::Utils::Unicode;

use Slim::Utils::Timers;
use Slim::Player::Playlist;


use JSON::XS::VersionOneAndTwo;

use Data::Dumper;
use URI::Split qw( uri_split uri_join );
use URI::Escape qw( uri_escape );

use constant INNUOS_API_URL => "http://localhost:8090/api/tidal_connect/get_stream_url";
use constant MP3_BITRATE => 320_000;

my $log   = logger('plugin.tidalconnectlink');
my $prefs = preferences('plugin.tidalconnectlink');

# Global variable to determine the track format (mp3 or flac) when obtaining the final Tidal URL
my $streamFormat;
my $streamFormatURL;
my $bitrate_info = 320;

my $transcodeSeek = 0;

sub isRemote { 1 }

sub canSeek { 1 }
sub canTranscodeSeek {
	main::DEBUGLOG && $log->is_debug && $log->debug( 'Can transcode seek: ' . $transcodeSeek );
	return $transcodeSeek;
}

# Don't allow looping
sub shouldLoop { 0 }

# Check if player is allowed to skip, using canSkip value from SN
sub canSkip { 1 }

sub scanUrl {
	# Preform prescanning of a track URL to discover up-to-date information.
	# It may be that the result actually represents a playlist, in which case the scanning process is expect to perform
	# any necessary scanning of the constituent tracks. This scanning is performed asynchronously,
	# the results being notified by callbacks.

	# We don't need URL scanning so we just return the current track

	my ($class, $url, $args) = @_;

	# main::DEBUGLOG && $log->is_debug && $log->debug('Scanning url '. $url);
	# main::DEBUGLOG && $log->is_debug && $log->debug('Before set: ' . $args->{song}->currentTrack()->content_type());

	Slim::Schema->clearContentTypeCache($url);
	$args->{song}->currentTrack()->content_type('unk');

	main::DEBUGLOG && $log->is_debug && $log->debug('AFTER set: ' . $args->{song}->currentTrack()->content_type());

	$args->{cb}->( $args->{song}->currentTrack());
}

# default buffer 3 seconds of 256kbps MP3/768kbps FLAC audio
my %bufferSecs = (
	flac => 80,
	flc => 80,
	mp3 => 32,
	mp4 => 40,
	mqa => 80
);

sub bufferThreshold {
	my ($class, $client, $url) = @_;

	$url = $client->playingSong()->track()->url() unless $url =~ /\.(fla?c|mp[34])/;
	my $ext = $1;

	main::DEBUGLOG && $log->is_debug && $log->debug($url);

	return ($bufferSecs{'mp4'} || $bufferSecs{$ext} || 40) * 3;
}

sub new {
	my $class  = shift;
	my $args   = shift;

	my $client    = $args->{client};

	my $song      = $args->{song};
	my $streamUrl = $song->streamUrl() || return;

	main::DEBUGLOG && $log->is_debug && $log->debug( 'Remote streaming track: ' . $streamUrl );

	my $sock = $class->SUPER::new( {
		url     => $streamUrl,
		song    => $song,
		client  => $client,
		bitrate => $bitrate_info * 1000,
	} ) || return;

	$log->warn( "Bitrate: " .  $bitrate_info * 1000);


	if ($streamFormat eq 'flac') {
		${*$sock}{contentType} = 'audio/flac';
		$transcodeSeek = 0;
	}
	elsif ($streamFormat =~ /mp4|aac/) {
		${*$sock}{contentType} = 'audio/mp4';
		$transcodeSeek = 0;
	}
	elsif ($streamFormat eq 'mqa') {
		${*$sock}{contentType} = 'audio/mqa';
		main::DEBUGLOG && $log->is_debug && $log->debug( 'Setting trancodeSeek: 1');
		$transcodeSeek = 1;
	}
	else {
		${*$sock}{contentType} = 'audio/mpeg';
		$transcodeSeek = 0;
	}

	main::DEBUGLOG && $log->is_debug && $log->debug( 'Mime: ' . ${*$sock}{contentType} );

	return $sock;
}


sub getFormatForURL {

 	# This function establishes the file format for the URL being played. Should be determined from the URL

 	my ($class, $url) = @_;

	my $format;
	main::DEBUGLOG && $log->is_debug && $log->debug('URL: ' . $url);
	main::DEBUGLOG && $log->is_debug && $log->debug('Stream format URL: ' . $streamFormatURL);
	main::DEBUGLOG && $log->is_debug && $log->debug('Stream format: ' . $streamFormat);
	# Check if the streamFormat has been filled by getTrackURL. If we don't know yet the format of the stream, return Unknown ("unk")
	if ($streamFormat && ($streamFormatURL eq $url)){  # || $url eq "")) {

		main::DEBUGLOG && $log->is_debug && $log->debug($streamFormat);

		if ($streamFormat eq 'aac' || $streamFormat eq 'mp4' ) {
			$format = 'mp4';
			$transcodeSeek = 0;
		}
		elsif ($streamFormat =~ /fla?c/i) {
			$format = 'flc';
			$transcodeSeek = 0;
		}
		else {
			$format = 'mqa';
			$transcodeSeek = 1;
		}

		main::DEBUGLOG && $log->is_debug && $log->debug($format);
		return $format;
	}

	return "unk";

}

# sub canDirectStreamSong {
#
# 	my ( $class, $client, $song ) = @_;
#
# 	my $songUrl = $song->streamUrl();
#
# 	main::DEBUGLOG && $log->is_debug && $log->debug($songUrl);
#
# 	return $class->SUPER::canDirectStream( $client, $song->streamUrl());
# }

sub getNextTrack {

	# Perform any operations, such as interaction with music services, to obtain all the necessary details of the
	# next track to be streamed. Like, scanUrl, the work of this method is performed asynchronously, the results being
	# notified by callbacks. This may determine that the stream URL will be different from the Track URL, and so may
	# also require a different handler.

	my ($class, $song, $successCb, $errorCb) = @_;

	my $url = $song->streamUrl();

	my $id = $class->crackUrl($url);

	my $params = {
		song        => $song,
		id          => $id,
		successCb   => $successCb,
		errorCb     => $errorCb,
		originalURL => $url,
	};

	getTrackURL($params);

}

sub getMetadataFor {

	my ( $class, $client, $url ) = @_;

	my $meta = {
		artist     => 'Innuos',
		album      => 'Music Server',
		title      => 'Streaming Tidal',
		cover      => '/plugins/TidalConnectLink/html/images/innuos_cover.png',
		bitrate    => '',
		samplerate => '',
		samplesize => '',
		type       => ''
	};

	return $meta;

}


# sub canDirectStreamSong {
# 	my ( $class, $client, $song ) = @_;
#
# 	main::DEBUGLOG && $log->is_debug && $log->debug();
#
# 	return $class->SUPER::canDirectStreamSong($client, $song);
#
# 	# We need to check with the base class (HTTP) to see if we
# 	# are synced or if the user has set mp3StreamingMethod
# 	# return $class->SUPER::canDirectStream( $client, $song->streamUrl() );
# }


# parseHeaders is used for proxied streaming
sub parseHeaders {
	my ( $self, @headers ) = @_;

	__PACKAGE__->parseDirectHeaders( $self->client, $self->url, @headers );

	return $self->SUPER::parseHeaders( @headers );
}

sub parseDirectHeaders {

	my ( $self, $client, $url, @headers ) = @_;

	my $oggType;

	# May get a track object
	if ( blessed($url) ) {
		($oggType) = $url->content_type =~ /(ogf|ogg|ops)/;
		$url = $url->url;
	}

	my $song = ${*$self}{'song'} if blessed $self;

	if (!$song && $client->controller()->songStreamController()) {
		$song = $client->controller()->songStreamController()->song();
	}

	my ($title, $bitrate, $metaint, $redir, $contentType, $length, $body);
	my ($rangeLength, $startOffset);

	foreach my $header (@headers) {

		# Tidy up header to make no stray nulls or \n have been left by caller.
		$header =~ s/[\0]*$//;
		$header =~ s/\r/\n/g;
		$header =~ s/\n\n/\n/g;

		main::DEBUGLOG && $log->is_debug && $log->debug("header-ds: $header");

		if ($header =~ /^(?:ic[ey]-name|x-audiocast-name):\s*(.+)/i) {

			$title = Slim::Utils::Unicode::utf8decode_guess($1);
		}

		elsif ($header =~ /^(?:icy-br|x-audiocast-bitrate):\s*(.+)/i) {
			if ($song && !$song->bitrate) {
				$bitrate = $1;
				$bitrate *= 1000 if $bitrate < 8000;
			}
		}

		elsif ($header =~ /^icy-metaint:\s*(.+)/i) {
			$metaint = $1;
		}

		elsif ($header =~ /^Location:\s*(.*)/i) {
			$redir = $1;
		}

		elsif ($header =~ /^Content-Type:\s*([^;\n]*)/i) {
			$contentType = $1;
		}

		elsif ($header =~ /^Content-Length:\s*(.*)/i) {
			$length = $1;
		}

		elsif ($header =~ /^Content-Range:\s+bytes\s+(\d+)-\d+\/(\d+)/i) {
			$startOffset = $1;
			$rangeLength = $2;
		}
	}

	# Content-Range: has predecence over Content-Length:
	if ($rangeLength) {
		$length = $rangeLength;
	}

	if ($song && $length) {
		my $seekdata = $song->seekdata();

		if ($startOffset && $seekdata && $seekdata->{restartOffset}
			&& $seekdata->{sourceStreamOffset} && $startOffset > $seekdata->{sourceStreamOffset})
		{
			$startOffset = $seekdata->{sourceStreamOffset};
		}
		else {
			$startOffset -= $song->currentTrack->audio_offset;
		}

		my $streamLength = $length;
		$streamLength -= $startOffset if $startOffset;
		$song->streamLength($streamLength);

		# However we got here, we want to know that we did not start at the beginning, if possible
		if ($startOffset) {


			# Assume saved duration is more accurate that by calculating from length and bitrate
			my $duration = Slim::Music::Info::getDuration($url);
			$duration ||= $length * 8 / $bitrate if $bitrate;

			if ($duration) {
				main::DEBUGLOG && $log->is_debug && $log->debug("Setting startOffest based on Content-Range to ", $duration * ($startOffset/$length));
				$song->startOffset($duration * ($startOffset/$length));
			}
		}
	}

	$contentType = Slim::Music::Info::mimeToType($contentType);

	if ( !$contentType ) {
		# Bugs 7225, 7423
		# Default contentType to mp3 as some servers don't send the type
		# or send an invalid type we don't include in types.conf
		$contentType = 'mp3';
	}

	if ( $streamFormat && $streamFormat eq 'mqa') {
		$contentType = 'mqa';
	}

	return ($title, $bitrate, $metaint, $redir, $oggType || $contentType, $length, $body);
}

#-----------------------------------------------------------
# Auxiliary functions
#-----------------------------------------------------------


sub getTrackURL {

	my $params = shift;
	my $id = $params->{id};
	my $apiURL = INNUOS_API_URL . "/" . $id;

	Slim::Networking::SimpleAsyncHTTP->new(
		# Upon successful call
		sub {
			my $response = shift;

			my $song = $params->{song};
			my $successCb = $params->{successCb};

			my $result = eval { from_json($response->content) };

			$@ && $log->error($@);

			main::DEBUGLOG && $log->is_debug  && $log->debug(Dumper($result));

			$song->streamUrl($result->{url});
			$song->duration($result->{duration});

			$streamFormat = ($result->{type});
			$streamFormatURL = ($params->{originalURL});

			if ($streamFormat eq 'mqa') {
				$transcodeSeek = 1;
			}
			else {
				$transcodeSeek = 0;
			}

			$bitrate_info = ($result->{bit_rate});
			$song->bitrate($bitrate_info);

			my $startTime = ($result->{start_time});

			if ($startTime) {
				$song->seekdata($song->getSeekData($startTime));
				main::DEBUGLOG && $log->is_debug && $log->debug('Setting seek data: ' . my $startTime);
			}

			if ($streamFormat =~ /mqa|mp4|aac|fla?c/i) {
				my $http = Slim::Networking::Async::HTTP->new;
				$http->send_request( {
					request     => HTTP::Request->new( GET => $result->{url} ),
					onStream    => $streamFormat =~ /mqa|fla?c/i ?
								   \&Slim::Utils::Scanner::Remote::parseFlacHeader :
								   \&Slim::Utils::Scanner::Remote::parseMp4Header,
					onError     => sub {
						my ($self, $error) = @_;
						$log->warn( "could not find $streamFormat header $error" );
						$params->{successCb}->();
					},
					passthrough => [ $song->track, { cb => $params->{successCb} }, $result->{url} ],
				} );
			} else {
				#$song->samplesize(24);
				#$song->samplerate(44.1);
				#main::DEBUGLOG && $log->debug('sample size http:' . $song->samplesize);
				#main::DEBUGLOG && $log->debug('sample rate http:' . $song->samplerate);
				$params->{successCb}->();
			}

			$successCb->();
		},
		# Upon Error with call
		sub {
			my ($http, $error) = @_;

			my $errorCb = $params->{errorCb};

			main::DEBUGLOG && $log->is_debug  && $log->debug(Dumper($error));

			$errorCb->("Error getting track url from innuOS")
		},
		{
			timeout => 15,
		},
	)->get($apiURL);

}

sub crackUrl {
	my ($class, $url) = @_;

	return unless $url;

	my $id = uri_escape($url);

	return ($id);
}


sub _getBitrate {
	my $ct = shift || '';

	return 800_000 if $ct =~ /fla?c/;
	return 320_000 if $ct =~ /aac|mp4/;

	return 256_00;
}


1;
