package Slim::Plugin::WiMP::ProtocolHandler;

# $Id: ProtocolHandler.pm 30836 2010-05-28 20:13:33Z agrundman $

use strict;
use base qw(Slim::Player::Protocols::HTTP);

use JSON::XS::VersionOneAndTwo;
use URI::Escape qw(uri_escape_utf8);
use Scalar::Util qw(blessed);

use Slim::Networking::SqueezeNetwork;
use Slim::Utils::Log;
use Slim::Utils::Misc;
use Slim::Utils::Prefs;
use Slim::Utils::Timers;

my $prefs = preferences('server');
my $log = Slim::Utils::Log->addLogCategory( {
	'category'     => 'plugin.wimp',
	'defaultLevel' => 'ERROR',
	'description'  => 'PLUGIN_WIMP_MODULE_NAME',
} );

sub isRemote { 1 }

sub getFormatForURL { return 'mp3' }

# default buffer 3 seconds of 256kbps audio
sub bufferThreshold {
	return 32 * ($prefs->get('bufferSecs') || 3); 
}

sub canSeek { 1 }

# To support remote streaming (synced players), we need to subclass Protocols::HTTP
sub new {
	my $class  = shift;
	my $args   = shift;

	my $client = $args->{client};
	
	my $song      = $args->{song};
	my $streamUrl = $song->streamUrl() || return;
	
	main::DEBUGLOG && $log->debug( 'Remote streaming WiMP track: ' . $streamUrl );

	my $sock = $class->SUPER::new( {
		url     => $streamUrl,
		song    => $args->{song},
		client  => $client,
		bitrate => 256_000,
	} ) || return;
	
	${*$sock}{contentType} = 'audio/mpeg';

	return $sock;
}

# Avoid scanning
sub scanUrl {
	my ( $class, $url, $args ) = @_;
	
	$args->{cb}->( $args->{song}->currentTrack() );
}

# Source for AudioScrobbler
sub audioScrobblerSource {
	my ( $class, $client, $url ) = @_;

	# P = Chosen by the user
	return 'P';
}

# Don't allow looping
sub shouldLoop { 0 }

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

sub handleDirectError {
	my ( $class, $client, $url, $response, $status_line ) = @_;
	
	main::DEBUGLOG && $log->debug("Direct stream failed: [$response] $status_line\n");
	
	$client->controller()->playerStreamingFailed($client, 'PLUGIN_WIMP_STREAM_FAILED');
}

sub _handleClientError {
	my ( $error, $client, $params ) = @_;
	
	my $song = $params->{song};
	
	return if $song->pluginData('abandonSong');
	
	# Tell other clients to give up
	$song->pluginData( abandonSong => 1 );

	if ( $error =~ /UserLoggedOutException/ ) {
	
		_handleUserLoggedOutException($client);
		return;
	}
	
	$params->{errorCb}->($error);
}

sub _handleUserLoggedOutException {
	my $client = shift;

	main::DEBUGLOG && $log->debug("User got logged out from WiMP - probably logged in elsewhere. Stop playback on all players playing WiMP.");

	my $userid;
	if (main::SLIM_SERVICE) {
		$userid = $client->playerData->userid;
	}

	foreach my $c ( Slim::Player::Client::clients() ) {

		# On SN, only stop players on the current account
		if (main::SLIM_SERVICE) {
			next if $userid != $c->playerData->userid;
		}

		if ( $c->isPlaying && $c->playingSong()->track()->url =~ /^wimp/ ) {

			Slim::Player::Source::playmode($c, 'stop');

			my $line1 = $c->string('PLUGIN_WIMP_ERROR');
			my $line2 = $c->string('PLUGIN_WIMP_ERROR_LOGGED_OUT');
	
			$c->showBriefly( {
				line => [ $line1, $line2 ],
				jive => { type => 'popupplay', text => [ $line2 ], duration => 5000},
			}, {
				scroll    => 1,
				firstline => 1,
				block     => 1,
				duration  => 5,
			} );
		}
	}
}


sub getNextTrack {
	my ( $class, $song, $successCb, $errorCb ) = @_;
	
	my $url = $song->track()->url;
	
	$song->pluginData( abandonSong   => 0 );
	
	my $params = {
		song      => $song,
		url       => $url,
		successCb => $successCb,
		errorCb   => $errorCb,
	};
	
	_getTrack($params);
}

sub _getTrack {
	my $params = shift;
	
	my $song   = $params->{song};
	my $client = $song->master();
	
	return if $song->pluginData('abandonSong');
	
	# Get track URL for the next track
	my ($trackId, $format) = _getStreamParams( $params->{url} );
	
	my $http = Slim::Networking::SqueezeNetwork->new(
		sub {
			my $http = shift;
			my $info = eval { from_json( $http->content ) };
			if ( $@ || $info->{error} ) {
				if ( main::DEBUGLOG && $log->is_debug ) {
					$log->debug( 'getTrack failed: ' . ( $@ || $info->{error} ) );
				}
				
				_gotTrackError( $@ || $info->{error}, $client, $params );
			}
			else {
				if ( main::DEBUGLOG && $log->is_debug ) {
					$log->debug( 'getTrack ok: ' . Data::Dump::dump($info) );
				}
				
				_gotTrack( $client, $info, $params );
			}
		},
		sub {
			my $http  = shift;
			
			if ( main::DEBUGLOG && $log->is_debug ) {
				$log->debug( 'getTrack failed: ' . $http->error );
			}
			
			_gotTrackError( $http->error, $client, $params );
		},
		{
			client => $client,
		},
	);
	
	main::DEBUGLOG && $log->is_debug && $log->debug('Getting next track playback info from SN');
	
	$http->get(
		Slim::Networking::SqueezeNetwork->url(
			'/api/wimp/v1/playback/getMediaURL?trackId=' . $trackId
		)
	);
}

sub _gotTrack {
	my ( $client, $info, $params ) = @_;
	
    my $song = $params->{song};
    
    return if $song->pluginData('abandonSong');
	
	# Save the media URL for use in strm
	$song->streamUrl($info->{url});

	# Save all the info
	$song->pluginData( info => $info );
	
	# Cache the rest of the track's metadata
	my $icon = Slim::Plugin::WiMP::Plugin->_pluginDataFor('icon');
	my $meta = {
		artist    => $info->{artist},
		album     => $info->{album},
		title     => $info->{title},
		cover     => $info->{cover} || $icon,
		duration  => $info->{duration},
		bitrate   => $info->{bitrate} . 'k CBR',
		type      => 'MP3 (WiMP)',
		info_link => 'plugins/wimp/trackinfo.html',
		icon      => $icon,
	};
	
	$song->duration( $info->{duration} );
	
	my $cache = Slim::Utils::Cache->new;
	$cache->set( 'wimp_meta_' . $info->{id}, $meta, 86400 );

	$params->{successCb}->();
	
	# trigger playback statistics update
	if ( $info->{duration} > 2) {
		# we're asked to report back if a track has been played halfway through
		my $params = {
			duration => $info->{duration} / 2,
			url      => $params->{url},
		};
		
		Slim::Utils::Timers::killTimers( $client, \&_reportPlayDuration );
		Slim::Utils::Timers::setTimer(
			$client,
			time() + $params->{duration},
			\&_reportPlayDuration,
			$params
		);
	}
}

sub _gotTrackError {
	my ( $error, $client, $params ) = @_;
	
	main::DEBUGLOG && $log->debug("Error during getTrackInfo: $error");

	return if $params->{song}->pluginData('abandonSong');

	_handleClientError( $error, $client, $params );
}

sub _reportPlayDuration {
	my ( $client, $params ) = @_;
	
	my $url = $client->playingSong()->track()->url();
	
	# only report if we're still playing the same track
	if ( $url && $url eq $params->{url} ) {
		
		main::DEBUGLOG && $log->is_debug && $log->debug("we're halfway through the track - reporting play duration");
		
		my $http = Slim::Networking::SqueezeNetwork->new(
			sub {
				my $http = shift;

				my $content = $http->content;
					
				if ( $content =~ /UserLoggedOutException/ ) {
					_handleUserLoggedOutException($client);
				}
					
				main::DEBUGLOG && $log->is_debug && $log->debug('reportPlayDuration returned: ' . $content)
			},
			sub {
				if ( main::DEBUGLOG && $log->is_debug ) {
					my $http = shift;
					$log->debug( 'reportPlayDuration failed: ' . $http->error );
				}
			},
			{
				client => $client,
			},
		);
		
		$http->get(
			Slim::Networking::SqueezeNetwork->url(
				'/api/wimp/v1/playback/reportPlayDuration?duration=' . $params->{duration}
			)
		);
		
	}
}

sub canDirectStreamSong {
	my ( $class, $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(), $class->getFormatForURL($song->track->url()) );
}

# 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 ( $class, $client, $url, @headers ) = @_;
	
	my $bitrate = 256_000;

	$client->streamingSong->bitrate($bitrate);

	# ($title, $bitrate, $metaint, $redir, $contentType, $length, $body)
	return (undef, $bitrate, 0, '', 'mp3');
}

# URL used for CLI trackinfo queries
sub trackInfoURL {
	my ( $class, $client, $url ) = @_;
	
	my $stationId;

	my ($trackId) = _getStreamParams( $url );
	
	# SN URL to fetch track info menu
	my $trackInfoURL = Slim::Networking::SqueezeNetwork->url(
		'/api/wimp/v1/opml/trackinfo?trackId=' . $trackId
	);
	
	if ( $stationId ) {
		$trackInfoURL .= '&stationId=' . $stationId;
	}
	
	return $trackInfoURL;
}

# Track Info menu
sub trackInfo {
	my ( $class, $client, $track ) = @_;
	
	my $url          = $track->url;
	my $trackInfoURL = $class->trackInfoURL( $client, $url );
	
	# let XMLBrowser handle all our display
	my %params = (
		header   => 'PLUGIN_WIMP_GETTING_TRACK_DETAILS',
		modeName => 'WiMP Now Playing',
		title    => Slim::Music::Info::getCurrentTitle( $client, $url ),
		url      => $trackInfoURL,
	);
	
	main::DEBUGLOG && $log->debug( "Getting track information for $url" );

	Slim::Buttons::Common::pushMode( $client, 'xmlbrowser', \%params );
	
	$client->modeParam( 'handledTransition', 1 );
}

# Metadata for a URL, used by CLI/JSON clients
sub getMetadataFor {
	my ( $class, $client, $url ) = @_;
	
	my $icon = $class->getIcon();
	
	return {} unless $url;
	
	my $cache = Slim::Utils::Cache->new;
	
	# If metadata is not here, fetch it so the next poll will include the data
	my ($trackId, $format) = _getStreamParams( $url );
	my $meta = $cache->get( 'wimp_meta_' . $trackId );
	
	if ( !$meta && !$client->master->pluginData('fetchingMeta') ) {
		# Go fetch metadata for all tracks on the playlist without metadata
		my @need;
		
		for my $track ( @{ Slim::Player::Playlist::playList($client) } ) {
			my $trackURL = blessed($track) ? $track->url : $track;
			if ( my ($id) = _getStreamParams( $trackURL ) ) {
				if ( !$cache->get("wimp_meta_$id") ) {
					push @need, $id;
				}
			}
		}
		
		if ( main::DEBUGLOG && $log->is_debug ) {
			$log->debug( "Need to fetch metadata for: " . join( ', ', @need ) );
		}
		
		$client->master->pluginData( fetchingMeta => 1 );
		
		my $metaUrl = Slim::Networking::SqueezeNetwork->url(
			"/api/wimp/v1/playback/getBulkMetadata"
		);
		
		my $http = Slim::Networking::SqueezeNetwork->new(
			\&_gotBulkMetadata,
			\&_gotBulkMetadataError,
			{
				client  => $client,
				timeout => 60,
			},
		);

		$http->post(
			$metaUrl,
			'Content-Type' => 'application/x-www-form-urlencoded',
			'trackIds=' . join( ',', @need ),
		);
	}
	
	#$log->debug( "Returning metadata for: $url" . ($meta ? '' : ': default') );
	
	return $meta || {
		bitrate   => '256k CBR',
		type      => 'MP3 (WiMP)',
		icon      => $icon,
		cover     => $icon,
	};
}

sub _gotBulkMetadata {
	my $http   = shift;
	my $client = $http->params->{client};
	
	$client->master->pluginData( fetchingMeta => 0 );
	
	my $info = eval { from_json( $http->content ) };
	
	if ( $@ || ref $info ne 'ARRAY' ) {
		$log->error( "Error fetching track metadata: " . ( $@ || 'Invalid JSON response' ) );
		return;
	}
	
	if ( main::DEBUGLOG && $log->is_debug ) {
		$log->debug( "Caching metadata for " . scalar( @{$info} ) . " tracks" );
	}
	
	# Cache metadata
	my $cache = Slim::Utils::Cache->new;
	my $icon  = Slim::Plugin::WiMP::Plugin->_pluginDataFor('icon');

	for my $track ( @{$info} ) {
		next unless ref $track eq 'HASH';
		
		# cache the metadata we need for display
		my $trackId = delete $track->{id};
		
		if ( !$track->{cover} ) {
			$track->{cover} = $icon;
		}

		my $bitrate = delete($track->{bitrate});
		
		my $meta = {
			%{$track},
			bitrate   => $bitrate . 'k CBR',
			type      => 'MP3 (WiMP)',
			info_link => 'plugins/wimp/trackinfo.html',
			icon      => $icon,
		};

		$cache->set( 'wimp_meta_' . $trackId, $meta, 86400 );
	}
	
	# Update the playlist time so the web will refresh, etc
	$client->currentPlaylistUpdateTime( Time::HiRes::time() );
	
	Slim::Control::Request::notifyFromArray( $client, [ 'newmetadata' ] );
}

sub _gotBulkMetadataError {
	my $http   = shift;
	my $client = $http->params('client');
	my $error  = $http->error;
	
	$client->master->pluginData( fetchingMeta => 0 );
	
	$log->warn("Error getting track metadata from SN: $error");
}

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

	return Slim::Plugin::WiMP::Plugin->_pluginDataFor('icon');
}

# SN only, re-init upon reconnection
sub reinit {
	my ( $class, $client, $song ) = @_;
	
	# Reset song duration/progress bar
	my $url = $song->track->url();
	
	main::DEBUGLOG && $log->is_debug && $log->debug("Re-init WiMP - $url");
	
	my $cache     = Slim::Utils::Cache->new;
	my ($trackId) = _getStreamParams( $url );
	my $meta      = $cache->get( 'wimp_meta_' . $trackId );
	
	if ( $meta ) {			
		# Back to Now Playing
		Slim::Buttons::Common::pushMode( $client, 'playlist' );
	
		# Reset song duration/progress bar
		if ( $meta->{duration} ) {
			$song->duration( $meta->{duration} );
			
			# On a timer because $client->currentsongqueue does not exist yet
			Slim::Utils::Timers::setTimer(
				$client,
				Time::HiRes::time(),
				sub {
					my $client = shift;
				
					$client->streamingProgressBar( {
						url      => $url,
						duration => $meta->{duration},
					} );
				},
			);
		}
	}
	
	return 1;
}

sub _getStreamParams {
	$_[0] =~ m{wimp://(.+)\.(m4a|aac|mp3)}i;
	return ($1, lc($2 || 'mp3') );
}

1;
