--[[----------------------------------------------------------------------------

WFBinaryRequester.lua

--------------------------------------------------------------------------------

ADOBE CONFIDENTIAL
------------------
Copyright 2016 Adobe Systems Incorporated
All Rights Reserved.

NOTICE: All information contained herein is, and remain the property of
Adobe Systems Incorporated and its suppliers, if any. The intellectual and
technical concepts contained herein are proprietary to Adobe Systems
Incorporated and its suppliers and may be covered by U.S. and Foreign
Patents, patents in process, and are protected by trade secret or copyright
law.  Dissemination of this information or reproduction of this material is
strictly forbidden unless prior written permission is obtained from
Adobe Systems Incorporated.

------------------------------------------------------------------------------]]

	-- RxLua
local Rx = require 'Rx'
local Observable = Rx.Observable
local ReplaySubject = Rx.ReplaySubject

	-- WF.core.logger
local WFLogger = require 'WFLogger'

	-- WF.core.utils
local WFArguments = require 'WFArguments'
local WFCloseSupport = require 'WFCloseSupport'
local KSTableUtils = require 'KSTableUtils'

--------------------------------------------------------------------------------

local check = assert( WFArguments.check )

local logger = WFLogger 'WFBinaryRequester'

--------------------------------------------------------------------------------

--[[!

	The following section contains constants defining the different parameters
	that can be used when requesting binaries.

]]--

--------------------------------------------------------------------------------

-- Each row has the following format
-- <Id>, <requestType>, <ozType>, <requestSize>, <ozSize>

local binaryTypes = {
	{ 'rendition_T2X', 'rendition', 'rendition', 'thumbnail2x', 'thumbnail2x' },
	{ 'rendition_640', 'rendition', 'rendition', '640', '640' },
	{ 'rendition_1280', 'rendition', 'rendition', '1280', '1280' },
	{ 'rendition_2048', 'rendition', 'rendition', '2048', '2048' },
	{ 'rendition_1080p', 'rendition', 'rendition', '1080p', '1080p' },
	{ 'rendition_1080p709', 'rendition', 'rendition', '1080p709', '1080p709' },
	{ 'rendition_720p', 'rendition', 'rendition', '720p', '720p' },
	{ 'rendition_360p', 'rendition', 'rendition', '360p', '360p' },
	{ 'rendition_fullsize', 'rendition', 'rendition', 'fullsize', 'fullsize' },
	{ 'rendition_pano', 'rendition', 'rendition', 'panorama', 'panorama' },
	{ 'proxy_fullsize', 'proxy', 'proxy', 'fullsize', 'fullsize' },
	{ 'proxy_2560', 'proxy', 'proxy', '2560', '2560' },
	{ 'proxy_1080p', 'proxy', 'proxy', '1080p', '1080p' },
	{ 'proxy_1080p709', 'proxy', 'proxy', '1080p709', '1080p709' },
	{ 'proxy_720p', 'proxy', 'rendition', '720p', '720p' },
	{ 'proxy_360p', 'proxy', 'proxy', '360p', '360p' },
	{ 'proxy_pano', 'proxy', 'proxy', 'panorama', 'panorama' },
	{ 'master', 'original', 'original', 'master', 'master' },
	{ 'xmp_develop', 'xmp_develop', 'xmp_develop', 'xmp_develop', 'xmp_develop' },
	{ 'xmp_sidecar', 'xmp_sidecar', 'xmp_sidecar', 'xmp_sidecar', 'xmp_sidecar' },
}

local ozBinaryTypeToCategoryMap = KSTableUtils.extractColumnAsTable( binaryTypes, 3 )
	-- Binary type (integer) -> category (string).
	
local ozBinaryTypeToSizeMap = KSTableUtils.extractColumnAsTable( binaryTypes, 5 )
	-- Binary type (integer) -> size/aspect-ratio (string).
	
--------------------------------------------------------------------------------

local function typeAndSizeKey( binaryType, binarySize )
	return tostring( binaryType ) .. ':' .. tostring( binarySize )
end

--------------------------------------------------------------------------------

local function createTypeAndSizeMap( theBinaryTypes )
	local map = {}
	
	for i, binaryType in ipairs( theBinaryTypes ) do
		local key = typeAndSizeKey( binaryType[ 2 ], binaryType[ 4 ] )
		map[ key ] = i
	end
	
	return map
end

--------------------------------------------------------------------------------

local typeAndSizeToIndexMap = createTypeAndSizeMap( binaryTypes )
	-- Oz binary type (string) : Oz binary size (string) -> binaryType index (integer)
	
--------------------------------------------------------------------------------

--[[!

	\brief Get the binaryType index for an oz binary type and a size
		
	\return index of the binaryType to use for a binary request
	
--]]

local function maybeBinaryTypeIndexForRequestType( requestedBinaryType, requestedBinarySize )
	
	check( '+string', '+string|number' )
	
	-- look up the binary type array index based on the binary type and binary size strings passed in
	local binaryTypeIndex = typeAndSizeToIndexMap[ typeAndSizeKey( requestedBinaryType, requestedBinarySize ) ]
	
	if not binaryTypeIndex then
		logger:errorf( "unable to determine binary type for type %s and size %s",
			requestedBinaryType, requestedBinarySize )
	end
	
	return binaryTypeIndex

end

--------------------------------------------------------------------------------

local function maybeRequestedBinaryTypeAndSizeFor( binaryTypeIndex )

	local binaryType = binaryTypes[ binaryTypeIndex ]
	
	if binaryType then
		return binaryType[ 2 ], binaryType[ 4 ]
	else
		return nil, nil
	end
	
end

--------------------------------------------------------------------------------

local function startDownload( self, ozAssetId, binaryType, ozBinaryUrl, ozRevId, downloadPriority )

	logger:debugf( "Downloading asset %s|%s", ozAssetId, binaryType )

	local downloadStream = Observable.throw( "Unable to start the binary download." )
	
	local requestCategory = binaryType and ozBinaryTypeToCategoryMap[ binaryType ]

	if not requestCategory then

		return Observable.throw( "Invalid or missing binary type." )

	end
	
	if ozBinaryUrl and ozRevId then
	
		if requestCategory == 'rendition' then
		
			downloadStream = self._downloader:getRenditionByUrl( 
					ozAssetId, 
					ozBinaryTypeToSizeMap[ binaryType ],
					ozBinaryUrl,
					ozRevId,
					downloadPriority )
					
		
		elseif requestCategory == 'proxy' then
	
			downloadStream = self._downloader:getProxyByUrl( 
					ozAssetId, 
					ozBinaryTypeToSizeMap[ binaryType ],
					ozBinaryUrl,
					ozRevId,
					downloadPriority )
		
		elseif requestCategory == 'original' then
		
			downloadStream = self._downloader:getMasterByUrl( ozAssetId, ozBinaryUrl, ozRevId, downloadPriority )
			
		elseif requestCategory == 'xmp_develop' then
		
			downloadStream = self._downloader:getXmpDevelopByUrl( ozAssetId, ozBinaryUrl, ozRevId, downloadPriority )
		
		elseif requestCategory == 'xmp_sidecar' then
		
			downloadStream = self._downloader:getXmpByUrl( ozAssetId, ozBinaryUrl, ozRevId, downloadPriority )
		
		end
		
	else
		
		if requestCategory == 'rendition' then
		
			downloadStream = self._downloader:getRendition( 
					ozAssetId, 
					ozBinaryTypeToSizeMap[ binaryType ],
					downloadPriority )
		
		elseif requestCategory == 'proxy' then
	
			downloadStream = self._downloader:getProxy( 
					ozAssetId, 
					ozBinaryTypeToSizeMap[ binaryType ],
					downloadPriority )
	
		elseif requestCategory == 'original' then
		
			downloadStream = self._downloader:getMaster( ozAssetId, downloadPriority )

		elseif requestCategory == 'xmp_develop' then
		
			downloadStream = self._downloader:getXmpDevelop( ozAssetId, downloadPriority )
		
		elseif requestCategory == 'xmp_sidecar' then
		
			downloadStream = self._downloader:getXmp( ozAssetId, downloadPriority )
			
		end
	end
	
	return downloadStream
	
end

--------------------------------------------------------------------------------

local function startDownloadToFile( self, ozAssetId, binaryType, ozBinaryUrl, ozRevId, downloadPriority, destPath )

	if not binaryType then

		return Observable.throw( "Cannot download to file: Missing binary type." )

	end

	local requestCategory = binaryType and ozBinaryTypeToCategoryMap[ binaryType ]

	if not requestCategory then

		return Observable.throw( string.format( "Cannot download to file: Invalid binary type %s", binaryType ) )

	end

	local maybeOzBinarySize = ozBinaryTypeToSizeMap[ binaryType ]

	-- initially we only support download to file for video proxies which are
	-- stored as renditions in the 720p case. We don't support any other
	-- rendition types yet.
	local isProxyLikeRendition = 
			requestCategory == 'rendition' and maybeOzBinarySize == '720p'

	if ozBinaryUrl and ozRevId then
	
		if requestCategory == 'proxy' then
	
			return self._downloader
					:downloadProxyByUrlToLocalFileStream( 
						ozAssetId,
						maybeOzBinarySize,
						ozBinaryUrl,
						ozRevId,
						downloadPriority,
						destPath )

		end
		
		if requestCategory == 'original' then
		
			return self._downloader
					:downloadOriginalByUrlToFileStream( 
						ozAssetId, 
						ozBinaryUrl, 
						ozRevId, 
						downloadPriority,
						destPath )

		end

		if isProxyLikeRendition then

			return self._downloader
					:downloadRenditionByURLToLocalFileStream( 
						ozAssetId, 
						maybeOzBinarySize,
						ozBinaryUrl,
						ozRevId,
						downloadPriority,
						destPath )

		end

	end
		
	if requestCategory == 'proxy' then

		return self._downloader
				:downloadProxyToLocalFileStream( 
					ozAssetId, 
					maybeOzBinarySize,
					downloadPriority, 
					destPath )

	end

	if requestCategory == 'original' then
	
		return self._downloader
				:downloadOriginalToFileStream( 
					ozAssetId,
					downloadPriority,
					destPath )

	end

	if isProxyLikeRendition then

		return self._downloader
				:downloadRenditionToLocalFileStream( 
					ozAssetId, 
					maybeOzBinarySize,
					downloadPriority,
					destPath )

	end

	return Observable.throw( 
			string.format( "Cannot download to file for unsupported requestCategory: %s (size: %s).", 
								requestCategory, maybeOzBinarySize ) )
	
	
end

--------------------------------------------------------------------------------

local function getAssetKey( ozAssetId, binaryType, ozBinaryUrl )

	local key = nil

	if ozAssetId and binaryType then
		key = string.format( "%s::s::%s::s::%s", ozAssetId, binaryType, tostring( ozBinaryUrl )  )
	end
	
	return key
	
end 

--------------------------------------------------------------------------------

local function getInFlightRequestByKey( self, key )

	return key and self._inFlightRequests[ key ] or nil

end 

--------------------------------------------------------------------------------

local function removeInFlightRequest( self, key )

	local requestRemoved = false

	if key then
	
		logger:debugf( "Deleting In-Flight Request key %s", key )
	
		local request = getInFlightRequestByKey( self, key )
		
		self._inFlightRequests[ key ] = nil
		
		if type( request ) == 'table' then
		
			request.downloadSubscription:dispose()
			requestRemoved = true
			
		end
	
	end
	
	return requestRemoved
	
end

--------------------------------------------------------------------------------

local function processRequest( self, key, request )

	self._inFlightRequests[ key ] = true
			
	-- Start the download.
	
	local downloadStream = 
		not request.maybeDestPath and
			startDownload( 
				self, 
				request.ozAssetId, 
				request.binaryType,
				request.ozBinaryUrl, 
				request.revisionId,
				request.httpRequestPriority ) or
			startDownloadToFile( 
				self,
				request.ozAssetId,
				request.binaryType,
				request.ozBinaryUrl,
				request.revisionId,
				request.httpRequestPriority,
				request.maybeDestPath )
			
	-- When the binary finishes downloading ( or errors out ), perform some cleanup.
		
	local cleanup = function()
	
		removeInFlightRequest( self, key )
		
		if key then
			self._numberOfRequesters[ key ] = nil
		end
		
	end 

	-- Map dat data.
	

	local outputDestination = request.maybeDestPath and "file" or "memory"
	
	local downloadSubscription = downloadStream
		:map( function( ... )
			return true, ...
		end )
		:catch( function( errorMessage, headers, linkKey, binaryUrl, revId )
			
			return Observable.just( false, errorMessage, revId, 
				{ 
					headers = headers, 
					linkKey = linkKey, 
					url = binaryUrl
				} )
			
		end )
		:subscribe( 
			function( okay, ... )
				
				logger:debugf( ( okay and "Received data" or "Error received" )  .. " for %s|%s|%s", request.ozAssetId, request.binarySize, request.binaryType )
			
				local ozType, ozSize = maybeRequestedBinaryTypeAndSizeFor( request.binaryType )

				if outputDestination == "file" then

					local filePathOrErrStr, _, addlInfo = ...

					self._binaryRequestToFileStream:onNext( okay, filePathOrErrStr, request.ozAssetId, ozType, ozSize, addlInfo )

					return

				end

				local maybeKSDataOrError, revId, addlInfo = ...
				
				self._binaryRequestStream:onNext( okay, maybeKSDataOrError, revId, request.ozAssetId, ozType, ozSize, addlInfo )
				
			end,
			
			cleanup, 
			
			cleanup
			
		)

	if key then
		request.downloadSubscription = downloadSubscription
		self._inFlightRequests[ key ] = request
	end
	
end


--============================================================================--

-- @mark WFBinaryRequester namespace

--[[!

	\class WFBinaryRequester

	\brief A helper class for managing download requests via a downloader class.
	
--]]

local WFBinaryRequester = {

	_downloader = false,
		-- (WFCatBinaryDownloader)
	
	_binaryRequestStream = false,
		-- (ReplaySubject) A stream that fires as binary requests are completed.
	
	_binaryRequestToFileStream = false,
		-- (ReplaySubject) A stream that fires as binary request to file are completed.
	
	_inFlightRequests = false,
		-- (table) A table of in-flight HTTP requests.
		-- key: (string) asset ID:binary type
		-- value: (table) request that includes the download subscription.
		
	_numberOfRequesters = false,
		-- (table) A table of containing the number of requesters who care about
		-- a given request.
		-- key: (string) asset ID:binary type
		-- value: (number) Requester count.
		
	WFCloseSupport.addCloseSupportMethods,	
	
}

--------------------------------------------------------------------------------

--[[!

	\brief Creates a new WF Binary Requester

	\param downloader (WFCatBinaryDownloader)
	
	\return WFBinaryRequester instance object
	
--]]


function WFBinaryRequester:__init( downloader )

	check( 'WFBinaryRequester', 'table' )

	logger:debugf( 'Creating WFBinaryRequester' )

	self._downloader = downloader

	self._binaryRequestStream = ReplaySubject( 1 )

	self._binaryRequestToFileStream = ReplaySubject( 1 )

	self._inFlightRequests = {}
	
	self._numberOfRequesters = {}
	
end

--------------------------------------------------------------------------------

--[[!

	\brief Closes the binary requester.
	
	\return void
	
--]]

function WFBinaryRequester:close()
										
	check( 'WFBinaryRequester' )
	
	local binaryRequestStream = self._binaryRequestStream
	self._binaryRequestStream = nil
	binaryRequestStream:onCompleted()
	binaryRequestStream:dispose()

	local binaryRequestToFileStream = self._binaryRequestToFileStream
	self._binaryRequestToFileStream = nil
	binaryRequestToFileStream:onCompleted()
	binaryRequestToFileStream:dispose()
	
end

--------------------------------------------------------------------------------

--[[!

	\brief Add a new request, or update an existing request.

	\param ozAssetId (string) The asset ID of the binary being requested.
		
	\param ozBinaryType (string) The Oz standard type of the requested binary. The following values are
	allowed:
		
		'rendition', 'proxy', 'original'
		
	\param ozBinarySize (string) The Oz standard size for the requested binary. Possible values depend on the
	value of ozBinaryType:
	
		If ozBinaryType == 'rendition':
			If image: 'thumbnail2x', '640', '1280', '2048', 'panorama'
			If video: 'thumbnail2x', '640', '1280', '2048', 'panorama', '1080p','1080p709', '720p', '360p'
		If ozBinaryType == 'proxy':
			If image: 'fullsize', '2560', 'panorama'
			If video: '1080p', '1080p709', '720p', '360p'
		If ozBinaryType == 'original':
			'master'
		If ozBinaryType == 'xmp_develop':
			'xmp_develop'
		If ozBinaryType == 'xmp_sidecar':
			'xmp_sidecar'
	
	\param httpRequestPriority (number) The priority of the request.
	
	\param optInfo (optional|table) An optional table of request info that gets added to the request.
	
	\return A function that can be called to cancel the request.
	
--]]

function WFBinaryRequester:requestBinary( ozAssetId, ozBinaryType, ozBinarySize, httpRequestPriority, optInfo )

	check( 'WFBinaryRequester', '+string', '+string', '+string|number', 'number', '?table' )
	
	return self:requestBinaryAtUrl( ozAssetId, ozBinaryType, ozBinarySize, nil, nil, httpRequestPriority, optInfo )
	
end

--------------------------------------------------------------------------------

local function requestBinaryAtUrlImp( self, returnNilIfRequestWasNotIssuedForBinaryType, ozAssetId, ozBinaryType, ozBinarySize, ozBinaryUrl, revisionId, httpRequestPriority, optInfo, maybeDestPath )

	check( 'WFBinaryRequester', 'boolean', '+string', '+string', '+string|number', '?string', '?string', 'number', '?table', '?string' )
	
	local binaryTypeIndex = maybeBinaryTypeIndexForRequestType( ozBinaryType, ozBinarySize )
	
	local key = getAssetKey( ozAssetId, binaryTypeIndex, ozBinaryUrl )

	if not key and returnNilIfRequestWasNotIssuedForBinaryType then

		logger:warnf( "don't know how to request binary %s.%s for %s", ozBinaryType, ozBinarySize, ozAssetId )

		return

	end
	
	optInfo = optInfo or {}
	
	-- Increment the number-of-clients-who-care-about-this-request count.
	
	if key then
		local numberOfRequesters = self._numberOfRequesters[ key ] or 0
		self._numberOfRequesters[ key ] = numberOfRequesters + 1
	end
	
	local cancelRequest = function()
	
		local requestCancelled = false
	
		-- Decrement the number-of-clients-who-care-about-this-request count.
	
		if key then
		
			local numberOfRequesters = self._numberOfRequesters[ key ] or 0
			numberOfRequesters = numberOfRequesters - 1
			
			if numberOfRequesters > 0 then
				requestCancelled = true
				self._numberOfRequesters[ key ] = numberOfRequesters
			else
				requestCancelled = removeInFlightRequest( self, key )
				self._numberOfRequesters[ key ] = nil
			end
			
		end
		
		-- Short-circuit additional attempts to cancel requests
		
		key = nil
		
		return requestCancelled
		
	end 
	
	-- Check to see if a request for this binary is already being processed. If not,
	-- request it.
	
	-- TODO [jandrew 2016-06-10]: Allow for updating priorities once the requests
	-- are scheduled. The previous version of the WFBinaryRequestQueue wasn't doing
	-- this either, so I'm okay with letting this slide for now.
		
	if key and self._inFlightRequests[ key ] == nil then
		
		logger:debugf( "Now requesting %s|%s.", ozAssetId, binaryTypeIndex )
		
		-- A unique request is determined by ozAssetId and binaryType. It's possible
		-- that multiple clients will request the same asset. In this case, they'll
		-- share the request.
		
		local request = {
			ozAssetId = ozAssetId,
			ozBinaryType = ozBinaryType,
			ozBinarySize = ozBinarySize,
			ozBinaryUrl = ozBinaryUrl,
			revisionId = revisionId,
			binaryType = binaryTypeIndex,
			httpRequestPriority = httpRequestPriority,
			cancelFn = cancelRequest,
			optInfo = optInfo,
			maybeDestPath = maybeDestPath
		}
		
		processRequest( self, key, request )
	
	end
	
	return cancelRequest
	
end

--------------------------------------------------------------------------------

function WFBinaryRequester:requestBinaryAtUrl( ozAssetId, ozBinaryType, ozBinarySize, ozBinaryUrl, revisionId, httpRequestPriority, optInfo )

	check( 'WFBinaryRequester', '+string', '+string', '+string|number', '?string', '?string', 'number', '?table' )

	return requestBinaryAtUrlImp( self, false, ozAssetId, ozBinaryType, ozBinarySize, ozBinaryUrl, revisionId, httpRequestPriority, optInfo )

end

--------------------------------------------------------------------------------

function WFBinaryRequester:maybeRequestBinaryAtUrl( ozAssetId, ozBinaryType, ozBinarySize, ozBinaryUrl, revisionId, httpRequestPriority, optInfo )

	check( 'WFBinaryRequester', '+string', '+string', '+string|number', '?string', '?string', 'number', '?table' )

	return requestBinaryAtUrlImp( self, true, ozAssetId, ozBinaryType, ozBinarySize, ozBinaryUrl, revisionId, httpRequestPriority, optInfo )

end

--------------------------------------------------------------------------------

function WFBinaryRequester:maybeRequestBinaryAtUrlToFile( destPath, ozAssetId, ozBinaryType, ozBinarySize, ozBinaryUrl, httpRequestPriority )

	check( 'WFBinaryRequester', '+string', '+string', '+string', '+string|number', '?string', 'number' )

	return requestBinaryAtUrlImp( self, true, ozAssetId, ozBinaryType, ozBinarySize, ozBinaryUrl, nil, httpRequestPriority, nil, destPath )

end

--------------------------------------------------------------------------------

--[[!

	\brief Retrieve a stream of binary assets that updates with every completed,
	success or error, binary request.

	\return <code>Observable<binary request></code> that updates as binary requests
	are fufilled. Each stream data event contains the following:
		
			okay
				- Boolean that indicates status of the request.
			maybeKSDataOrError
				- Either an error string, or the requested binary.
			revId
				- The revision ID of the asset downloaded from Oz.
			ozAssetId
				- The asset ID of the requested asset.
			ozBinaryType
				- String denoting the Oz standard binary type ( 'rendition', 'proxy', 'original' ).
			ozBinarySize
				- String denoting the Oz standard size of the binary ( 'thumbnail2x', '2048', etc. )
	
	Example:
	
		binaryRequester:getBinaryRequestStream()
		
			:map( function( okay, maybeKSDataOrError, revId, ozAssetId, ozBinaryType, ozBinarySize )
			
				if okay
				
					-- process binary payload
				
				else
					
					-- process error
				
				end
			
			end )
	
--]]

function WFBinaryRequester:getBinaryRequestStream()

	check( 'WFBinaryRequester' )

	return self._binaryRequestStream

end

--------------------------------------------------------------------------------

function WFBinaryRequester:getBinaryRequestToFileStream()

	check( 'WFBinaryRequester' )

	return self._binaryRequestToFileStream

end

--------------------------------------------------------------------------------

function WFBinaryRequester:getInFlightRequests()

	check( 'WFBinaryRequester' )

	return self._inFlightRequests

end

--------------------------------------------------------------------------------

return Rx.Class 'WFBinaryRequester'( WFBinaryRequester )
