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

WFOzRenditionServerUtils.lua

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

ADOBE CONFIDENTIAL
------------------
Copyright 2012 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.

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

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

	-- WF.core.os
local WFPlatformInfo = require 'WFPlatformInfo'

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

--------------------------------------------------------------------------------
local derefOpt = KSTableUtils.derefOpt
local usingWFDocStore = WFCapabilities.getCapability 'OzDocStore.disable'

--============================================================================--
--[[ Don't get too attached to this namespace. In all likelihood, this
	will disappear into the private portions of \c WFOzRenditionServer when
	that refactoring is completed.
]]--

-- @mark WFOzRenditionServerUtils

local WFOzRenditionServerUtils = {}

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

local check = assert( WFArguments.check )

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

local cacheEpochLog = WFLogger 'WFOzRenditionServerUtils :: cacheEpoch'

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

local cacheEpochSize

if WFPlatformInfo.isMac then
	cacheEpochSize = 50 * 1024 * 1024
elseif WFPlatformInfo.isiOS then
	cacheEpochSize = 20 * 1024 * 1024
else
	-- GRUNGE [arahn 2012-08-28]: What are sensible defaults for other platforms?
	cacheEpochSize = 20 * 1024 * 1024
end

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

local cacheEpochBs = KSTableUtils.makeFullyWeak()
	-- key (OzDocStore)
	-- value: B< number >

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

function WFOzRenditionServerUtils.cacheEpochB( --[[OzDocStore]] docStore,
											   --[[WFIndexStore]] indexStore )

	-- Do we have a cached cache epoch behavior already?
	
	local catalogPath = docStore:path()
	
	local resultB = WFCloseSupport.fetchCached( cacheEpochBs, catalogPath )
	if resultB then
		return resultB:autoCloseRelayB()
	end
	
	-- We don't need the cache monitoring to be precise, but we do need it
	-- to be efficient. Profile data shows that this is a major source of
	-- DB thread CPU consumption in larger catalogs, so we monitor cache epoch
	-- via a calmed behavior. The consequence of this is that we might write more
	-- data into an epoch than intended, but that's relatively harmless.
	
	cacheEpochLog:infof( "created cacheEpochB for doc store at %s", catalogPath )
	
	local maxEpochB = indexStore:queryB {
		sql = 'SELECT max( epoch ) FROM cacheReferences',
		calm = 10,
	}
	:closingMapB( function( rows )
		return rows and rows[ 1 ] and rows[ 1 ][ 1 ] or 1
	end )
	
	if cacheEpochLog:will_debug() then
		maxEpochB:ownedOnDataB( maxEpochB, function( maxEpoch )
			cacheEpochLog:debug( "maxEpochB updated to %s", maxEpoch )
		end )
	end
	
	local epochSizeB = maxEpochB:closingMapSwitchB( function( maxEpoch )
		return indexStore:queryB {
			sql = 'SELECT epoch, sum( size ) FROM cacheReferences WHERE epoch = ?',
			calm = 10,
			maxEpoch,
		}
	end )
	:closingMapB( function( rows )
		local row1 = rows and rows[ 1 ]
		if row1 then
			return ( row1[ 1 ] or 1 ), ( row1[ 2 ] or 0 )
		else
			return 1, 0
		end
	end )
	:addCloseTarget( maxEpochB )
	
	if cacheEpochLog:will_debug() then
		epochSizeB:ownedOnDataB( epochSizeB, function( epoch, size )
			cacheEpochLog:debugf( "epochSizeB updated to %s, %s", epoch, size )
		end )
	end
	
	resultB = epochSizeB:closingMapB( function( epoch, size )
		
		if size >= cacheEpochSize then
			cacheEpochLog:infof( "Epoch %s too large (size = %s), starting new epoch %s",
								 epoch, size, epoch + 1 )
			return epoch + 1
		else
			cacheEpochLog:infof( "Epoch %s size %s OK", epoch, size )
			return epoch
		end
	
	end )
	
	docStore:addCloseTarget( resultB )
	
	cacheEpochBs[ catalogPath ] = resultB
	
	return resultB:autoCloseRelayB()
	
end

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

-- We map the rendition names down to a smaller set of rendition kinds so that we can employ
-- common processing where appropriate.

local renditionKindForRenditionName = {
	thumbnail = 'thumbnail',
	preview = 'preview',
	master = 'master',
	thumbnail2x = 'thumbnail',
}

local ozLinkNameForRenditionName = {
	thumbnail = "/rels/rendition_type/144h",
	preview = "/rels/rendition_type/1024",
	master = "/rels/master",
	thumbnail2x = "/rels/rendition_type/thumbnail2x",
}

WFOzRenditionServerUtils.renditionKindForRenditionName = renditionKindForRenditionName
WFOzRenditionServerUtils.ozLinkNameForRenditionName = WFCapabilities.callFilters( "WFOzRenditionServerUtils.ozLinkNameForRenditionName", ozLinkNameForRenditionName )

--------------------------------------------------------------------------------
-- Should we bother touching the rendition?

function WFOzRenditionServerUtils.shouldTouchRendition( --[[string]] ozRenditionName )

	return renditionKindForRenditionName[ ozRenditionName ] ~= 'thumbnail'
	
end

local WFOzRenditionServerUtils_shouldTouchRendition =
					WFOzRenditionServerUtils.shouldTouchRendition

--------------------------------------------------------------------------------
-- Touch a rendition (place it in the current epoch). This operation is
-- fire-and-forget.

function WFOzRenditionServerUtils.touchRendition( --[[OzDocStore]] docStore,
												  --[[B< number >]] cacheEpochB,
												  --[[string]] photoId,
												  --[[string]] renditionName,
												  --[[table]] optDocValue,
												  --[[string]] optSize )

	-- TODO [scouten 2014-06-20]: Passing in the cacheEpochB here is messy IMHO.
	-- Not clear if we take ownership of it or not, but in the cases that I
	-- encountered just now, the caller does not retain ownership. As previously
	-- written, those behavior (relays) got GC'd causing mustClose violation
	-- warnings. So, for now, we'll make sure this func closes the behavior.

	check( 'WFDocStore|OzDocStore|OzDocStoreViaWFDocStore|table', '_', 'string', 'string', '?table', '?string' )

	-- Touch a rendition (place it in the current epoch). This operation is
	-- fire-and-forget.
	
	-- Don't bother touching thumbnails. We don't space manage them.
	
	if not WFOzRenditionServerUtils_shouldTouchRendition( renditionName ) then
		cacheEpochB:close()
		return
	end

	-- Circle back if we don't have doc value. (It's better to provide optDocValue
	-- if you can!)
	
	if not optDocValue then
	
		docStore:docValueCheckTypeP( 'asset', photoId )
			:whenResolvedDo( function( status, docValue )
			
				if status == 'data' then
					WFOzRenditionServerUtils.touchRendition( docStore, cacheEpochB, photoId,
															 renditionName, docValue,
															 optSize )
				end

			end ):runUntilDone()
		
		return
	
	end

	-- If this item is already in current epoch, ignore.

	local renditionInfo
	
	if usingWFDocStore then
		renditionInfo = derefOpt( optDocValue, '_localOnly', 'renditions', renditionName )
	else
		renditionInfo = optDocValue
						  and optDocValue._localOnly
						  and optDocValue._localOnly[ renditionName ]
	end

	local epochNow = cacheEpochB:valueNow()
	cacheEpochB:close()

	if not epochNow then
		cacheEpochLog:warn( "cacheEpochB had no value ... not touching rendition" )
		return
	end

	if not renditionInfo then
		cacheEpochLog:debugf( "rendition %s has no value ... not touching rendition", renditionName )
		return
	end

	if renditionInfo and renditionInfo.epoch == epochNow
		and ( renditionInfo.size == optSize or not optSize )
	then
		cacheEpochLog:debugf( "not touching %s for photo %s ... already in current epoch",
							  renditionName, photoId )
		return
	end
	
	-- Update cache info.
	
	cacheEpochLog:debugf( "cache touching %s for photo %s, moving to epoch %d with size %s",
						  renditionName, photoId, epochNow, optSize )

	local overlay
	
	if usingWFDocStore then
		overlay = {
			renditions = {
				[ renditionName ] = {
								epoch = epochNow,
								size = optSize,
							}
			}
		}
	else
		overlay = {
			[ renditionName ] = {
							epoch = epochNow,
							size = optSize,
						}
		}
	end
	
	docStore:applyOverlayP {
		
		type = 'asset',
		_id = photoId,
		_slot = 'main',
		
		_localOnly = overlay
	
	}
	:runUntilDone()

end

--------------------------------------------------------------------------------
-- @mark Index Store Schema (md5)
--------------------------------------------------------------------------------

local function coreMD5FromDocValue( docValue )
	
	local renditions = { 'thumbnail', 'thumbnail2x', 'preview', 'master' }
	local results = {}
	local localOnly = docValue._localOnly
	
	for index, rendition in ipairs( renditions ) do
		local md5 = derefOpt( localOnly, rendition, 'md5' )
		results[ index ] = md5
	end
	
	return unpack ( results, 1, 4 )

end

--------------------------------------------------------------------------------
--[[
	These constants are only used when existing photos (imported on 2011-08-12
	and before are in the system and we didn't capture the rendition size.
]]--

local defaultSizeForRendition = {
	original = 20000000,
	preview = 150000,
	thumbnail = 12000,
	thumbnail2x = 48000,
}

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

function WFOzRenditionServerUtils.getFullMD5HashedKey ( md5, docId )
	return string.format ("%s_%s", md5, docId)
end

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

function WFOzRenditionServerUtils.filterIndexDefinitions( tableDefinitions, indexDefinitions )

	local OzCatalogUploadStatus = require 'OzCatalogUploadStatus'
	
	tableDefinitions.coreMD5 =  {
	
		sql = [[CREATE TABLE coreMD5( thumbnail, thumbnail2x, preview, master )]],
	
		hasOneRowPerDocument = true,
	
		map = function( docValue, emit )
			
			if docValue.type == 'asset' and not docValue._deleted then
	
				local thumbnail, thumbnail2x, preview, master = coreMD5FromDocValue( docValue )
				
				if thumbnail or thumbnail2x or preview or master then
					emit( thumbnail, thumbnail2x, preview, master )
				end
			
			end
				
		end,
	
	}
	
	tableDefinitions.cacheReferences =  {
	
		sql = [[CREATE TABLE cacheReferences( md5, renditionName, size, epoch )]],
	
		map = function( docValue, emit )
	
			local localOnly = docValue._localOnly
			
			local serverLinks =
				  ( docValue._serverProvidedMetadata
				and docValue._serverProvidedMetadata.links ) or {}
	
			local function emitForRenditionName( renditionName, ozNameForRendition )
			
				local renditionInfo = derefOpt( localOnly, 'renditions', renditionName ) or derefOpt( localOnly, renditionName )
				if not renditionInfo then return end
				
				
				-- we aren't making destructive edits to originals yet, so we do not upload
				-- originals other than the base original
				
				local renditionKey
				
				if renditionName == 'latestOriginal' then
					renditionKey = OzCatalogUploadStatus.checkAndCorrectLocalPath(
									localOnly.latestOriginal
										and localOnly.latestOriginal.filePath )
				elseif renditionName == 'original' then
					-- Temporary check to correct the local file path and replace
					-- 'Adobe Carousel Library.cr' with 'Adobe Revel Library.cr'
			
					-- FIXME [ngautam 2011-11-15] This should really be done via an upgrade process
					-- that updates the database with the correct file paths but this is not
					-- feasible in the current time frame.
					renditionKey = OzCatalogUploadStatus.checkAndCorrectLocalPath( localOnly.originalFilePath )
				else
					renditionKey = renditionInfo.md5
				end
	
				if renditionInfo and renditionKey then
				
					local serverLinksForRendition = serverLinks[ ozNameForRendition ]
					local shouldUpload
					
					if renditionName ~= 'latestOriginal' then
						shouldUpload = ( not serverLinksForRendition )
							or serverLinksForRendition.invalid
							or renditionInfo.revId == 'pending'
					else
						-- We don't upload the latest original
						shouldUpload = false
					end
						
				
					emit( renditionKey,
						  renditionName,
						  renditionInfo.size or defaultSizeForRendition[ renditionName ] or 1000000,
						  renditionInfo.epoch or 1,
						  shouldUpload and 0 or 1 )
				
--					cacheReferencesLog:debug( "photoId = %s | renditionName = %s | size = %s | epoch  %s | hasUploaded = %s",
--						docValue._id or docValue._docId, renditionName, renditionInfo.size, renditionInfo.epoch or 1,
--						not shouldUpload )
				
				end
			
			end
	
			emitForRenditionName( 'thumbnail', '/rels/rendition_type/144h' )
			emitForRenditionName( 'thumbnail2x', '/rels/rendition_type/thumbnail2x' )
			emitForRenditionName( 'preview', '/rels/rendition_type/1024' )
--			emitForRenditionName( 'original', '/rels/master' )
--			emitForRenditionName( 'latestOriginal', '' )
	
		end,
	
	}
	
	indexDefinitions.cacheReferences_epoch =
		[[CREATE INDEX cacheReferences_epoch ON cacheReferences( epoch DESC, size )]]
	
	indexDefinitions.cacheReferences_md5 =
		[[CREATE INDEX cacheReferences_md5 ON cacheReferences( md5 ASC, docId )]]
	
	indexDefinitions.cacheReferences_potentialVictims =
		[[CREATE INDEX cacheReferences_potentialVictims
			ON cacheReferences( epoch ASC, size DESC, docId )]]
	
end

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

return WFOzRenditionServerUtils
