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

ADOBE CONFIDENTIAL
------------------
Copyright 2014 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'

	-- WF.core.rxlua
local WFRxPromiseBridge = require 'WFRxPromiseBridge'

    -- WF.core.os
local KSPathUtils = require 'KSPathUtils'
local WFFileUtils = require 'WFFileUtils'

	-- WF.core.utils
local WFCloseSupport = require 'WFCloseSupport'
local KSTableUtils = require 'KSTableUtils'
local KSData = require 'KSData'
local WFUnknown = require 'WFUnknown'


	-- WF.core.reactive
local KSReactive = require 'KSReactive'
local WFWorkerThread = require 'WFWorkerThread'

	-- WF.oz_client.catalog
local OzCatalogUrls = require 'OzCatalogUrls'
local OzCatHelperFns = require 'OzCatHelperFns'

	-- WF.oz_client.catalog.avatar_server
local WFOzAvatarServerUtils = require 'WFOzAvatarServerUtils'

	-- WF.sqlite.data_cache
local KSDataCache_lazy -- lazy loaded
local WFOzRenditionServerUtils = require 'WFOzRenditionServerUtils'
local WFOzRenditionServerUtils_getFullMD5HashedKey = WFOzRenditionServerUtils.getFullMD5HashedKey

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

local asKSPromise = WFRxPromiseBridge

local WFCloseSupport_fetchCached = assert( WFCloseSupport.fetchCached )
local WFCloseSupport_hasBeenOrWillBeClosed = assert( WFCloseSupport.hasBeenOrWillBeClosed )

local jpegDecodeService = WFWorkerThread 'KSJpegDecodeServiceEPInit.lua'

local kAvatarTypeFallbackOrder = { 'large2x', 'large', 'medium2x',
									'medium', 'small2x', 'small' }
									
--==============================================================================

local avatarServerMethods = {

	_catalog = false,

	_docStore = false,
	
	_indexStore = false,
	
	_dataCache = false,
	
	_avatarBehaviorCache = false,
	
	_md5BehaviorCache = false,
	
	_cacheEpochB = false,
	
	WFCloseSupport.addCloseSupportMethods,
}

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

function avatarServerMethods:__init( args )

	self._catalog = assert( args.catalog )
	
	self._docStore = assert( args.docStore )
	
	self._indexStore = assert( args.indexStore )
	
	self._dataCache = assert( self:avatarDataCache() )
	
	self._avatarBehaviorCache = KSTableUtils.makeFullyWeak()
	
	self._md5BehaviorCache = KSTableUtils.makeFullyWeak()
	
	self:setOwned {
		_cacheEpochB = WFOzAvatarServerUtils.cacheEpochB( self._docStore, self._indexStore )
	}
	
end

function avatarServerMethods:getAvatarDataCachePath()

	local cachePath
	
	self._catalog._wfCatalogStream

		:take( 1 )
		
		:subscribe( function( wfCatalog )
		
			if wfCatalog:fileSpec():docStorePath():sub( 1, 8 ) == ':memory:' then
	
				cachePath = ':memory:'
		
			elseif wfCatalog:fileSpec():docStorePath():sub( 1, 6 ) == ':temp:' then
	
				cachePath = ':temp:'
				
			else
		
				local documentsDir = KSPathUtils.getStandardFilePath 'carouselDocuments'
		
				local catalogDir = KSPathUtils.child( documentsDir, wfCatalog:ozCatalogSpec():catalogId() )
				catalogDir = KSPathUtils.child( catalogDir, "renditions" )
	
				WFFileUtils.mkdir( catalogDir )
	
				cachePath = KSPathUtils.child( catalogDir, "avatarCache" )
				
			end
			
		end )
		
		:dispose()

	return cachePath
		
end

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

function avatarServerMethods:avatarDataCache()
	
	local result = self._dataCache
	
	if not result then
	
		local cachePath = self:getAvatarDataCachePath()

		KSDataCache_lazy = KSDataCache_lazy or require 'KSDataCache'

		result =  KSDataCache_lazy( cachePath )
		
		self:setOwned { _dataCache = result }
		
	end
	
	return result
	
end

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

local function suppressNilB( sourceB )

	local resultB = sourceB
		:closingFilterE( function( v ) return v ~= nil end )
		:closingHoldValueNowB( sourceB )
	
	return resultB

end

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

local function first( x )
	return x
end

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

-- We compute the current MD5 hash by or-ing together the MD5 hashes from the
-- database lookup (a) and the hint or pass through (b).

local function orMD5( a, b )

	if a then
		return a
	end
	
	return b

end

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


local function rawDataCacheGetB( self, md5, debugRetainCount, highPriority )

	return self._dataCache:getB( md5, debugRetainCount, highPriority )

end

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

local function getFromDataCacheB( self, md5 )

	if not md5 then
		return KSReactive.constantB( nil )
	end
	
	local b = rawDataCacheGetB( self, md5, false, false )
	
	b = b:closingMapB( function( value, reason )
		if value == nil then
			return reason
		else
			return value
		end
	end )
		
	return b
	
end

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

local function getAvatarInfoWithFallback( docValue, avatarType )

	local avatars = KSTableUtils.derefOpt( docValue, '_localOnly', 'avatars' )
		
	if not avatars then
		return nil, nil
	end
	
	local info = KSTableUtils.derefOpt( avatars, avatarType )
	
	if info then
		return info, avatarType
	end
	
	local index = 0
	for i,v in pairs( kAvatarTypeFallbackOrder ) do
	
		if v == avatarType then
		
			index = i
			break
			
		end
		
	end
	
	for i = index, #kAvatarTypeFallbackOrder do
	
		info = KSTableUtils.derefOpt( avatars, kAvatarTypeFallbackOrder[ i ] )
		
		if info then
			return info, kAvatarTypeFallbackOrder[ i ]
		end
	end
		
	return info, avatarType
	
end

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

local function getAvatarUrlWithFallback( docValue, avatarType )

	local links = KSTableUtils.derefOpt( docValue, '_serverProvidedMetadata', 'links' )
	
	local avatarUrl, newAvatarType
	
	-- let's try to fetch the URL directly
	
	avatarUrl = KSTableUtils.derefOpt( links, '/rels/avatars/' .. avatarType, 'href' )
	
	if avatarUrl then
		return avatarUrl, avatarType
	end
	
	-- if not successful, fallback to smaller avatar
	local index = 0
	for i,v in pairs( kAvatarTypeFallbackOrder ) do
	
		if v == avatarType then
		
			index = i
			break
			
		end
		
	end
	
	for i = index, #kAvatarTypeFallbackOrder do
	
		avatarUrl = KSTableUtils.derefOpt( links, '/rels/avatars/' .. kAvatarTypeFallbackOrder[ i ], 'href' )
		
		if avatarUrl then
		
			newAvatarType = kAvatarTypeFallbackOrder[ i ]
			break
			
		end
		
	end
	
	return avatarUrl, newAvatarType
	
end


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

local function fetchAndDecodeAvatarWithMD5B( self, feedbackuserId, avatarType, md5 )

	local dataB = getFromDataCacheB( self, md5 )
	
	return dataB:closingMapSwitchB( function( ksData )
	
		if not ksData then
			return KSReactive.constantB( nil )
		end
		
		if type( ksData ) == 'string' then
			return KSReactive.constantB( ksData )
		end
		
		local decodedP = jpegDecodeService:requestP( 'decode', ksData )
			
		-- TODO [pkamin 2014-08-28]: cache decoded image
		
		return decodedP:untilResolvedB( 'data', nil )
		
	end )

end

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

local function downloadAvatarForUrlP( self, avatarUrl )

	local sequencing = 'Low'

	local opts = {
		followRedirects = true,
	}

	local p = self._catalog:getCatUser()
		:flatMap( function( user )
			
			return user:getBinary( avatarUrl, opts, sequencing )
			
		end)
		:call( asKSPromise )
		
	return p

end

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

local function md5ForFeedbackUserB( self, userId, avatarType )

	local cachedB = WFCloseSupport_fetchCached( self._md5BehaviorCache, userId )
							
	if cachedB then
		return cachedB:autoCloseRelayB()
	end

	local docValueB = self._docStore:docValueB( 'feedbackuser', userId )
		
	local md5B = docValueB:closingKnownMapB( function( docValue )
	
		local info, newAvatarType = getAvatarInfoWithFallback( docValue, avatarType )
		
		local md5 = KSTableUtils.derefOpt( info, 'md5' )
		
		if md5 then
		
			WFOzAvatarServerUtils.touchAvatar( self._docStore, self._cacheEpochB, userId,
												newAvatarType, docValue )
												
		end
		
		return md5
		
	end )
	
	-- TODO [pkamin 2014-08-28]: touch avatar
	
	local variableB = KSReactive.variableB( nil )
	
	local resultB = KSReactive.closingDataCallB( orMD5, md5B, variableB )
	
	rawset( resultB, '_WFOzAvatarServer_md5Variable', variableB )
	
	self._md5BehaviorCache[ userId ] = resultB
	
	return resultB:autoCloseRelayB()
	
end

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

-- Bypass the doc store for doing an MD5 update. We also do this when the
-- data cache read fails.

local function updateMD5ForAvatar( self, feedbackuserId, ... )

	local cachedB = WFCloseSupport_fetchCached( self._md5BehaviorCache, feedbackuserId )
	
	if not cachedB then
		return
	end
	
	local variableB = rawget( cachedB, '_WFOzAvatarServer_md5Variable' )
	
	if variableB and not WFCloseSupport_hasBeenOrWillBeClosed( variableB ) then
		variableB:set( ... )
	end

end

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

local function addLocalLinkForAvatarP( self, userId, avatarType, data, md5, avatarUrl )

	local _, revId = avatarUrl:match( '/avatars/([0-9a-f]+)/([0-9a-f]+)/.*' )
	
	local avatarOverlay = {
		md5 = md5,
		revId = revId,
		epoch = self._cacheEpochB:valueNow(),
		size = KSData.length( data ),
	}
	
	local resultP = self._docStore:applyOverlayToDocumentP {
		docType = 'feedbackuser',
		docId = userId,
		overlay = {
			_localOnly = {
				avatars = {
					[ avatarType ] = avatarOverlay
				}
			}
		},
	}
	
	return resultP
	
end

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

local function shouldDownloadAvatar( docValue, avatarType )

	local avatarUrl = KSTableUtils.derefOpt( docValue, '_serverProvidedMetadata', 'links',
						'/rels/avatars/' .. avatarType, 'href' )
	
	if avatarUrl then
	
		local _, revId = avatarUrl:match( '/avatars/([0-9a-f]+)/([0-9a-f]+)/.*' )
		
		local localRevId = KSTableUtils.derefOpt( docValue, '_localOnly',
			'avatars', avatarType, 'revId' )
			
		if localRevId ~= revId then
			return true
		end
		
	end
	
	return false
	
end

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

local function makeAvatarDownloaderB( self, userId, avatarType )

	local docValueB = self._docStore:docValueB( 'feedbackuser', userId )
	
	-- Get the information for the download
	
	local avatarUrlB = docValueB:closingKnownMapB( function( docValue )

		if not docValue or not shouldDownloadAvatar( docValue, avatarType ) then
			return nil
		end
		
		local avatarUrl, newAvatarType = getAvatarUrlWithFallback( docValue, avatarType )
		
		if avatarUrl then
		
			local imageBaseUrl = OzCatalogUrls.baseServerUrl
			
			avatarUrl = avatarUrl and
				OzCatHelperFns.checkForRelativeUrlAndAppendBase( avatarUrl, imageBaseUrl )
		end
		
		return avatarUrl, newAvatarType

	end )
	
	-- Do the download when appropriate
	
	local downloadB = avatarUrlB
		:closingKnownMapSwitchB( function( avatarUrl, newAvatarType )
		
			if not avatarUrl then
				return KSReactive.constantB( nil )
			end
			
			local resultP = downloadAvatarForUrlP( self, avatarUrl )
			
			resultP = resultP:waitForP( function( status, data )
	
				if status ~= 'data' or not data then
					return
				end
		
				local avatarMD5 = data:digest( 'MD5' )
				
				avatarMD5 = WFOzRenditionServerUtils_getFullMD5HashedKey (avatarMD5, userId)

				local storeP = self._dataCache:putP( avatarMD5, data )
									
				-- Remove the store operation from limbo. This also eliminates our
				-- ability to cancel, but there really isn't much sense in canceling
				-- storing the data once we've read it.
										
				storeP:runUntilDone()
				
				updateMD5ForAvatar( self, userId, avatarMD5 )
			
				local updateDocStoreP = storeP:mapDataP( function()
				
					return addLocalLinkForAvatarP( self,
						userId, newAvatarType, data, avatarMD5, avatarUrl )
						
				end )
				
				-- Similarly, if we've got the data and we've stored, then we want to update
				-- the doc store.
				
				updateDocStoreP:runUntilDone()
				
				-- And then we tell any code that is still interested that it should wait for
				-- updateDocStoreP to complete. Note that we don't need storeP to complete since
				-- we have the data in the local cache.
				
				return updateDocStoreP
			
			end )
			
			resultP
				:whenResolvedDo()
				:setPriority( 'very_low' )
				:runUntilDone()
				
			
			return resultP
				:untilResolvedB( 'data', false )
				
		end )
	
	return downloadB
	
end

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

local function getAvatarB( self, userId, avatarType )

	local avatarMd5B = md5ForFeedbackUserB( self, userId, avatarType )
	
	local decodedB = avatarMd5B:closingKnownMapSwitchB( function( md5 )
	
		return fetchAndDecodeAvatarWithMD5B( self, userId, avatarType, md5 )
		
	end )
	
	local imageB = decodedB:closingMapB( function( value )
	
		if value == 'cache miss' then
		
			-- TODO [pkamin 2014-08-28]: remove local link
		end
		
		-- Map away status values
		
		if type( value ) == 'string' then
			value = nil
		end
		
		return value

	end )
	
	local downloadB = makeAvatarDownloaderB( self, userId, avatarType )
	
	imageB = KSReactive.closingDataCallB(
					first,
					imageB,
					downloadB:closingRawMapB( function() return 'data', true end  ) )
	
	-- Don't let nil values while we're going to the next item slip through.
	-- We can start out nil, but we won't slip back to being nil.
	
	imageB = suppressNilB( imageB )
	
	return imageB

end

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

-- Check to see whether the proxy reports itself as closed.

local function checkProxy( obj )

	if not obj then
		return true
	end
	
	if type( obj ) ~= 'userdata' then
		return true
	end
	
	if obj:proxyHasBeenOrWillBeClosed() then
		return false
	end
	
	return true

end

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

function avatarServerMethods:avatarB( userId, avatarType )

	local cachedB = KSReactive.fetchCachedBehavior(
					self._avatarBehaviorCache, userId, checkProxy )
			
	if cachedB then
		return cachedB:autoCloseRelayB()
	end
	
	local avatarB = getAvatarB( self, userId, avatarType )
		:closingFilterB( function( val ) return val ~= nil end, WFUnknown.value() )
		
	self._avatarBehaviorCache[ userId ] = avatarB
	
	return avatarB:autoCloseRelayB()
	
end

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

return Rx.Class 'WFOzAvatarServer'( avatarServerMethods )
