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

LrmCatOriginals.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.

----------------------------------------------------------------------------]]--
	
	-- WF.core.reactive
local KSPromises = require 'KSPromises'

local WFArguments = require "WFArguments"
local KSData = require "KSData"
local WFFileUtils = require "WFFileUtils"
local WFLogger = require "WFLogger"
local KSPathUtils = require "KSPathUtils"
local KSPrefs = require "KSPrefs"

local OzCatHelperFns = require "OzCatHelperFns"
local OzCatalogUploadStatus = require "OzCatalogUploadStatus"
local OzEpoch = require "OzEpoch"

local WFOzRenditionServerUtils = require 'WFOzRenditionServerUtils'
local WFCatTrashUtils = require 'WFCatTrashUtils'

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

-- @mark OzCatalogService class

local OriginalsMethods = {}

local OzCatalogServiceArg = 'OzCatalogServiceImp'

local OzCatHelperFns_nilP = OzCatHelperFns.nilP
local OzCatHelperFns_DISABLE_MOST_FETCHES = OzCatHelperFns.DISABLE_MOST_FETCHES
local OzCatHelperFns_processHttpRequestResult = OzCatHelperFns.processHttpRequestResult
local WFOzRenditionServerUtils_touchRendition = WFOzRenditionServerUtils.touchRendition
local OzEpoch_cacheEpochB = OzEpoch.cacheEpochB
local OzCatHelperFns_findAndCorrectOriginalPath = OzCatHelperFns.findAndCorrectOriginalPath

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

local logger = WFLogger 'OzCatalogService :: download original'
--	:setLevel 'debug'

--------------------------------------------------------------------------------
--[[!
	\brief Synchronously checks whether the latest original (full-size) photo
		is available, either locally or on the server.
	
	\param docValue (table) The asset's document description.
	\param checkLocally (optional, boolean) When true, check only
		local availability of the latest original. Otherwise, also check
		on the server.
	
	\return (boolean) Returns \c true if the original is available, \c false if not
]]--

function OriginalsMethods.originalAvailable( catalog, docValue, checkLocally )

    WFArguments.check( OzCatalogServiceArg, 'table', '?boolean' )

    local localPath = docValue
        and docValue._localOnly
        and docValue._localOnly.originalFilePath
        
    local localPath_latestOriginal = docValue
        and docValue._localOnly
        and docValue._localOnly.latestOriginal
        and docValue._localOnly.latestOriginal.filePath
        
    local ozUrl = docValue
        and docValue._serverProvidedMetadata
        and docValue._serverProvidedMetadata.links
        and docValue._serverProvidedMetadata.links[ "/rels/master" ]
        and docValue._serverProvidedMetadata.links[ "/rels/master" ].href

    if not ozUrl then
        ozUrl = docValue
            and docValue._localOnly
            and docValue._localOnly.copyMasterFrom
    end

	-- another preference: Fetch url from trash section of localOnly
	if not ozUrl then

		-- If OzUrl is not present in spm/links, It can happen for restored assets in expired account state or paused sync state, then fetch it from localOnly section

		local masterLinkKey = '/rels/master'
		local WFCatTrashUtils_getOriginalAssetUrlForLinkKey = assert( WFCatTrashUtils.getOriginalAssetUrlForLinkKey )
		local masterLink = WFCatTrashUtils_getOriginalAssetUrlForLinkKey( docValue, masterLinkKey)

		ozUrl = masterLink

	end

    -- If we don't have a local path to a valid original or a server url to
	-- download from, return false.
    
    if OzCatalogUploadStatus.assetOriginalIsValid( docValue ) then

        -- No destructive edits have been made.

        if checkLocally then

            if localPath then
                return true
            else
                return false
            end

        elseif not ( localPath or ozUrl ) then
            return false
        else
            return true
        end

    else

        -- Destructive edits were made. If we have the latest original locally
		-- or on the server, return true.

        if checkLocally then

            if ( localPath_latestOriginal and OzCatalogUploadStatus.assetLatestOriginalIsValid( docValue ) ) then
                return true
            else
                return false
            end

        elseif ( localPath_latestOriginal and OzCatalogUploadStatus.assetLatestOriginalIsValid( docValue ) ) or ozUrl then
            return true
        else
            return false
        end
    end

end

--------------------------------------------------------------------------------
--[[!
	\brief Asynchronously checks whether the latest original (full-size) photo
		is available either locally or on the server.
	
	\param photoId (string) The asset's unique ID.
	\param checkLocally (optional, boolean) When \c true, check only
		local availability of the latest original. Otherwise, also check
		on the server.
	
	\return <code>P< boolean ></code> A Promise object that resolves to
		\c true if the original is available, \c false if not
]]--

function OriginalsMethods.originalAvailableP( catalog, photoId, checkLocally )

	if not photoId then
		return OzCatHelperFns_nilP
	end

	local docValueP = catalog:docStore():docValueOnceP( 'asset', photoId )

	return docValueP:mapDataP( function( status, docValue )
	
        return 'data', OriginalsMethods.originalAvailable( catalog, docValue, checkLocally )

	end )

end

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

local function downloadAndCacheOriginalPathP( catalog, originalUrl, photoId,
											  imageInfo, renditionName, ozUrl_spm, priority )

	logger:debugf( "downloadAndCacheOriginalPathP %s from %s", photoId, originalUrl )

	assert( renditionName == 'original' or renditionName == 'latestOriginal' )

	local optArgs = {
		requestPriority = priority
	}
	
	-- Set timeout for download based on ~100K internet connection.
	-- If we can't muster that, we probably don't want the download.
	-- Adjust if that turns out to be problematic.
	
	local fileSizeHint = imageInfo
					 and imageInfo.importSource
					 and imageInfo.importSource.fileSize
	
	if type( fileSizeHint ) == 'number' and fileSizeHint > 0 then

		optArgs.timeout = math.max( 60, fileSizeHint / 12500 )

		logger:debugf( "downloadAndCacheOriginalPathP calculated timeout value of %.2f for file size %d",
					   optArgs.timeout, fileSizeHint )

	end
	
	-- Log and handle results

	local resultP = catalog:getBinaryP( originalUrl, optArgs )
	
	resultP = resultP:observeP( function( status, data, headers )
	
		logger:debugf( "downloadAndCacheOriginalPathP %s resolved with %s %s",
					   photoId, status, headers and headers.status_code )

		-- If we resolved in error with a 404, the URL for this master
		-- exists but the master cannot be found.

		if status == 'error' and headers and headers.status_code == '404' then
		
			-- Mark this asset's original as missingInOz.
			
			local overlayForMissingOriginal = {
				_localOnly = {
					[ renditionName ] = {
						missingInOz = true,
						},
					},
				}
			
			catalog:docStore():applyOverlayToDocumentP {
				docType = 'asset',
				docId = photoId,
				overlay = overlayForMissingOriginal,
			}:runUntilDone()
			
		end

		OzCatHelperFns_processHttpRequestResult( catalog, status, data, headers )
		
	end )

	local resultPP = KSPromises.callP( function( originalData, originalsFolderDocuments )

			local captureDate = imageInfo.captureDate:sub( 1, 10 )
			
			local downloadOriginals = KSPrefs[ "DownloadOriginals" ]
			
			local subPath = KSPathUtils.child( captureDate:sub( 1, 4 ), captureDate:sub( 1, 7 ) ) -- yyyy, yyyy-mm
			subPath = KSPathUtils.child( subPath, captureDate ) -- yyyy-mm-dd
						
			local folder
			local overlay = {
				_localOnly = {
					original = {},
				},
			}
										
			if renditionName == 'original' then
			
				-- If we just downloaded the base original:
						
				if downloadOriginals then

					folder = KSPathUtils.child( originalsFolderDocuments, subPath )
					overlay._localOnly.original.isInCacheFolder = false

				else

					assert( false, "We shouldn't use this code path" )
					
					folder = KSPathUtils.child( originalsFolderDocuments, subPath )
					overlay._localOnly.original.isInCacheFolder = true

				end
			
			elseif renditionName == 'latestOriginal' then
			
				-- We just downloaded the latest version of the original.
				-- Replace the old latest version we had with this version.
				
				overlay._localOnly.latestOriginal = {}

				if downloadOriginals then

					folder = KSPathUtils.child( originalsFolderDocuments, subPath )
					overlay._localOnly.latestOriginal.isInCacheFolder = false

				else

					assert( false, "We shouldn't use this code path" )
					
					folder = KSPathUtils.child( catalog:getOriginalsFolderCaches(), subPath )
					overlay._localOnly.latestOriginal.isInCacheFolder = true

				end

			end
			
			WFFileUtils.mkdir( folder )
		
			local leafName = OzCatHelperFns.importFileNameFromPhotoInfo( imageInfo )
			local destPath = KSPathUtils.child( folder, leafName )
			
			destPath = KSData.saveToUniquelyNamedFile( originalData, destPath )
			
			local fileCreationDate = imageInfo
					 and imageInfo.importSource
					 and imageInfo.importSource.fileCreationDate
					
			if fileCreationDate then
				WFFileUtils.setFileAttributes( destPath, { fileCreationDate = fileCreationDate } )
			end
			
			local dataSize = KSData.length( originalData )
			
			if renditionName == 'original' then
			
				local epochB = OzEpoch_cacheEpochB( catalog )
				overlay._localOnly.originalFilePath = destPath
				overlay._localOnly.original.ozUrl = ozUrl_spm
													or imageInfo
													and imageInfo._serverProvidedMetadata
													and imageInfo._serverProvidedMetadata.links[ "/rels/master" ]
													and imageInfo._serverProvidedMetadata.links[ "/rels/master" ].href
				overlay._localOnly.original.size = dataSize
				overlay._localOnly.original.epoch = epochB:valueNow()
				epochB:close()
				
			elseif renditionName =='latestOriginal' then
			
				local epochB = OzEpoch_cacheEpochB( catalog )
				overlay._localOnly.latestOriginal.filePath = destPath
				overlay._localOnly.latestOriginal.ozUrl = imageInfo
													and imageInfo._serverProvidedMetadata
													and imageInfo._serverProvidedMetadata.links[ "/rels/master" ]
													and imageInfo._serverProvidedMetadata.links[ "/rels/master" ].href
				overlay._localOnly.latestOriginal.size = dataSize
				overlay._localOnly.latestOriginal.epoch = epochB:valueNow()
				epochB:close()
				-- This download could be replacing a previously downloaded and now invalid
				-- latest original. If there is an older original, we need to ensure that if it has been
				-- uploaded, we delete it. And only then update the doc store with the path
				-- to the new original.
			
				local olderOriginalPath = imageInfo
							and imageInfo._localOnly
							and imageInfo._localOnly.latestOriginal
							and imageInfo._localOnly.latestOriginal.filePath

				olderOriginalPath = OzCatalogUploadStatus.checkAndCorrectLocalPath( olderOriginalPath )
						
				if olderOriginalPath then

					-- Check if this has been uploaded - unanswered at this point?
					-- Our method of determining whether this needed to be uploaded was to check
					-- against the server links.

					-- FIXME [ngautam 2011-10-26]: Check if this original has been uploaded
					-- and if possible complete the upload before deleting the asset.
				
					WFFileUtils.moveToTrash( olderOriginalPath )

				end

			end
			
			WFFileUtils.setDoNotBackUpFlag( destPath )
			
			-- Q&D fix for memory issue: Close this right away and then load up
			-- a memory-mapped representation instead.
			
			-- FIXME [scouten 2011-08-09]: The data should be just written directly
			-- to disk. Tracked by https://www.pivotaltracker.com/story/show/16794587.
			
			originalData:close()
		
			-- FIXME [scouten 2011-06-03]: Should store this result locally and
			-- hand back the mapped KSData. Don't try this with your 39MP image
			-- on an iPad.
			
			-- Write the URL for the original that was just downloaded. We will use this
			-- value to determine whether the current original is current or not.
			
			-- Load up a memory-mapped version of what we just wrote out.
		
			logger:debugf( "Wrote overlay to document %s", photoId )
			
			return catalog:docStore():applyOverlayToDocumentP( {
					docType = 'asset',
					docId = photoId,
					overlay = overlay,
				} )
				
				:mapDataP ( function()
				
					return 'data', destPath
					
				end )
		
		end, resultP, catalog:getOriginalsDocumentsFolderP() )

	return resultPP
	
end

--------------------------------------------------------------------------------
--[[!
	\brief Retrieves the local path of an original (full-size) photo document,
		and optionally downloads the asset itself.
 
	\param photoId (string) The asset's unique ID.
	\param doNotFetch (optional, boolean) When true, do not retrieve the asset.
	\param priority (number) HTTP request priority, check WFRxHttpClient for more details
	
	\return <code>P< string ></code></code>
		A Promise object that resolves on completion to the
		path of the downloaded asset, or to nil if the asset has not been
		previously downloaded and has not been fetched by this call. 
]]--

function OriginalsMethods.originalDataPathP( catalog, photoId, doNotFetch, priority )

	WFArguments.check( 'OzCatalogService', 'string', 'boolean', 'number', '?boolean' )
	
	if OzCatHelperFns_DISABLE_MOST_FETCHES or not photoId then
		return OzCatHelperFns_nilP
	end
	
	local docValueOnceP = catalog:docStore():docValueOnceP( 'asset', photoId )

	local resultP = docValueOnceP:mapDataP( function( status, docValue )
	
		if not docValue then
			return 'data', nil -- or is this an error state, too?
		end
		
		-- First preference: Use original that's already stored locally.
		
		local localPath = OzCatHelperFns_findAndCorrectOriginalPath( docValue )
						
		local localPath_latestOriginal = docValue
						and docValue._localOnly
						and docValue._localOnly.latestOriginal
						and docValue._localOnly.latestOriginal.filePath

		localPath_latestOriginal = OzCatalogUploadStatus.checkAndCorrectLocalPath( localPath_latestOriginal )

		-- FIXME [scouten 2011-07-24]: Should also check MD5 hash?
		
		-- This is to specify whether we are downloading the base or latest original
		-- when we call downloadAndCacheOriginalPathP.

		local renditionName
		
		if OzCatalogUploadStatus.assetOriginalIsValid( docValue ) then
		
			renditionName = 'original'
			if localPath and WFFileUtils.exists( localPath ) then
				WFOzRenditionServerUtils_touchRendition( catalog:docStore(), OzEpoch_cacheEpochB( catalog ),
														 photoId, 'original', docValue )
				return 'data', localPath
			end
			
		else
		
			renditionName = 'latestOriginal'
		
			if OzCatalogUploadStatus.assetLatestOriginalIsValid( docValue ) then
			
				if localPath_latestOriginal and WFFileUtils.exists( localPath_latestOriginal ) then
					WFOzRenditionServerUtils_touchRendition( catalog:docStore(), OzEpoch_cacheEpochB( catalog ),
															 photoId, 'latestOriginal', docValue )
					return 'data', localPath_latestOriginal
				end
			
			end
			
		end
		
		if doNotFetch then
			return 'data', nil
		end
		
		-- Second preference: Download from Oz.
		
		local ozUrl = docValue
						and docValue._serverProvidedMetadata
						and docValue._serverProvidedMetadata.links
						and docValue._serverProvidedMetadata.links[ "/rels/master" ]
						and docValue._serverProvidedMetadata.links[ "/rels/master" ].href
		
		if not ozUrl then
			ozUrl = docValue
				and docValue._localOnly
				and docValue._localOnly.copyMasterFrom
		end

		-- another preference: Fetch url from trash section of localOnly
		if not ozUrl then

			-- If OzUrl is not present in spm/links, It can happen for restored assets in expired account state or paused sync state, then fetch it from localOnly section

			local masterLinkKey = '/rels/master'
			local WFCatTrashUtils_getOriginalAssetUrlForLinkKey = assert( WFCatTrashUtils.getOriginalAssetUrlForLinkKey )
			local masterLink = WFCatTrashUtils_getOriginalAssetUrlForLinkKey( docValue, masterLinkKey)

			ozUrl = masterLink

		end

		if ozUrl then

			local resultP = downloadAndCacheOriginalPathP( catalog, ozUrl, photoId, docValue, 
				renditionName, nil, priority )
			
			return resultP

		end

		-- Bummer: Original doesn't exist anywhere we can reach.
		
		return 'error', "Original can not be found"
			-- FIXME [scouten 2011-07-25]: Does this need to be localized?
	
	end )
	
	return resultP

end

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

return OriginalsMethods
