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

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

	-- WF.core.utils
local KSIdGenerator = require 'KSIdGenerator'
local WFArguments = require 'WFArguments'
local KSTableUtils = require 'KSTableUtils'
local KSPathUtils = require "KSPathUtils"

local WFTableMerge = require 'WFTableMerge'

	-- WF.core.lua
local KSData = require 'KSData'

	-- WF.core.os
local KSDate = require 'KSDate'
local WFDeviceInfo = require 'WFDeviceInfo'
local WFFileUtils = require 'WFFileUtils'
local WFPlatformInfo = require 'WFPlatformInfo'

	-- WF.web.json
local KSJsonDecoder = require 'KSJsonDecoder'

	-- WF.oz_client.catalog
local OzEpoch = require 'OzEpoch'

	-- WF.oz_client.utils
local OzFilePaths = require 'OzFilePaths'
local OzCatHelperFns = require 'OzCatHelperFns'
local WFOzRenditionServerUtils = require 'WFOzRenditionServerUtils'
local WFOzRenditionServerUtils_getFullMD5HashedKey = WFOzRenditionServerUtils.getFullMD5HashedKey

local logger = require "WFLogger" "LrmImport"
	--:setLevel( 'debug' )

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

local LrmImport = {}

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

local OzEpoch_cacheEpochB = OzEpoch.cacheEpochB
local check = assert( WFArguments.check )

local derefOpt = assert( KSTableUtils.derefOpt )

local kImportStatusNewAsset = 'new'
local kImportStatusDuplicateAsset = 'duplicate'

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


local function saveRenditionSetP( catalog, asset, epochNow, dataCache, newRevId, assetDocId )

--	check( 'OzCatalogServiceImp', 'table' )
	
	local renditions = asset.renditions or {}
	
	local function saveRenditionP( rendition )
	
		check( 'table' )
		
		local renditionName = rendition.renditionName
		local renditionData = rendition.renditionData
		
		assert( KSData.isData( renditionData ) )
		
		local renditionMd5 = renditionData:digest( 'MD5' )
		renditionMd5 = WFOzRenditionServerUtils_getFullMD5HashedKey (renditionMd5, assetDocId)

		local renditionInfo = {
			md5 = renditionMd5,
			revId = newRevId,
			epoch = epochNow,
			size = KSData.length( renditionData ),
		}
		
		asset.docValue._localOnly[ renditionName ] = renditionInfo
		
		return dataCache:putP( renditionMd5, renditionData )
			:mapDataP_new( function()
			
				return asset
				
			end )
		
	end
		
	return KSPromises.allP( saveRenditionP, renditions )

		:mapDataP_new( function()
		
			return asset

		end )

end

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

local function saveProxyP( catalog, asset, epochNow, documentsPath, dontSave )

	if not asset.proxy then
	
		return KSPromises.resolvedPromiseP( 'data', asset )
		
	end
	
	local docValue = asset.docValue
	
	local proxy = asset.proxy
	
	local proxySize = 0

	local destPath
	
	if dontSave then

		local proxyFileAttributes = WFFileUtils.fileAttributes( proxy )
		proxySize = proxyFileAttributes.fileSize
		destPath = proxy

	else

		local folder = OzFilePaths.originalsFolderForDate( documentsPath, docValue.captureDate )

		WFFileUtils.mkdir( folder )

		local leafName = OzCatHelperFns.importFileNameFromPhotoInfo( docValue, '_proxy.dng' )

		destPath = KSPathUtils.child( folder, leafName )

		local saveDataFn = function( proxyData, path )
			destPath = KSData.saveToUniquelyNamedFile( proxyData, path )
		end


		-- Wrap this in a pcall so that we always close proxyData, even if
		-- an error occurred in saving the data.

		if KSData.isData( proxy ) then

			local proxyData = asset.proxy
			proxySize = KSData.length( proxyData )

			local success, result = pcall( saveDataFn, proxyData, destPath )

			if not success then
				return KSPromises.resolvedPromiseP( 'error', result )
			end

		elseif type( proxy ) == 'string' and WFFileUtils.exists( proxy ) == 'file' then

			local proxyFileAttributes = WFFileUtils.fileAttributes( proxy )
			proxySize = proxyFileAttributes.fileSize

			local success, result = pcall( WFFileUtils.copyToUniquelyNamedFile, proxy, destPath, not WFPlatformInfo.isAndroid )

			if not success then

				return KSPromises.resolvedPromiseP( 'error', result )

			end

		else
			return KSPromises.resolvedPromiseP( 'error', 'No valid proxy data type' )
		end
	end
	
	WFTableMerge.applyOverlay( docValue, {
		_localOnly = {
			proxy = {
				epoch = epochNow,
				size = proxySize,
				filePath = destPath,
			}
		}
	} )
	
	return KSPromises.resolvedPromiseP( 'data', asset )
	
end

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

local function saveOriginalP( catalog, asset, epochNow, documentsPath, dontSave )

	if not asset.original then
		return KSPromises.resolvedPromiseP( 'data', asset )
	end

	local fileSize
	local docValue = asset.docValue
	local original = asset.original
	local originalfd = asset.originalfd
	if WFPlatformInfo.isAndroid then
		fileSize = asset.fileSize
	end

	local originalSize = 0
	local destPath
	
	if dontSave then

		local originalFileAttributes = WFFileUtils.fileAttributes( original )
		originalSize = originalFileAttributes.fileSize
		destPath = original

	else
		local folder = OzFilePaths.originalsFolderForDate( documentsPath, docValue.captureDate )

		WFFileUtils.mkdir( folder )

		local leafName = OzCatHelperFns.importFileNameFromPhotoInfo( docValue )

		destPath = KSPathUtils.child( folder, leafName )

		if KSData.isData( original ) then

			originalSize = KSData.length( original )

			-- Wrap this in a pcall so that we always close originalData, even if
			-- an error occurred in saving the data.

			local success, result = pcall( KSData.saveToUniquelyNamedFile, original, destPath )

			if result then
				destPath = result
			end

			if not success then
				logger:warnf( "Saving failed. Error: %s", result )
				return KSPromises.resolvedPromiseP( 'error', result )
			end

		elseif type( original ) == 'string' then
			--Android specific comment
			--be careful using original, for OS < AndroidQ original will have absolute path
			--for OS >= AndroidQ original will have relative path to file.
			local success, result
			if WFPlatformInfo.isAndroid then
				originalSize = fileSize
				success, result = pcall( WFFileUtils.copyToUniquelyNamedFileUsingFileDescriptor, originalfd, destPath )
			else
				local originalFileAttributes = WFFileUtils.fileAttributes( original )
				originalSize = originalFileAttributes.fileSize
				success, result = pcall( WFFileUtils.copyToUniquelyNamedFile, original, destPath, true )
			end
			
			if result then
				destPath = result
			end

			if not success then

				return KSPromises.resolvedPromiseP( 'error', result )

			end

		else
			return KSPromises.resolvedPromiseP( 'error', 'No valid original data type' )
		end
	end

	WFTableMerge.applyOverlay( docValue, {
		_localOnly = {
			originalFilePath = destPath,
			original = {
				epoch = epochNow,
				size = originalSize,
				isInCacheFolder = false,
			}
		}
	} )
	
	return KSPromises.resolvedPromiseP( 'data', asset )
	
end

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

local function createImportResult( assetId, localUrl, importStatus )

	return {
		assetId = assetId,
		importStatus = importStatus,
		localUrl = localUrl
	}
end

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

local function addAssetToCatalogP( catalog, asset, userId, options )

--	check( 'OzCatalogServiceImp', 'table' )
	
	-- check KSData
	-- check args
	
	-- check local url
	
	local dontSave = options.dontSave or false
	local deviceId = asset.deviceId
	
	local bypassSha256Check = options.bypassSha256Check or false
	
	if not deviceId then
		deviceId = WFDeviceInfo.uniqueDeviceId()
	end
	
	local docValue = assert( asset.docValue )
	local sha256 = derefOpt( docValue, 'importSource', 'sha256' )
	
	if not docValue.captureDate then
		docValue.captureDate = KSDate.timeInIso8601Format( nil, false )
	end
	
	docValue.originalCaptureDate = docValue.captureDate
	
	if not docValue.userCreated then
		docValue.userCreated = KSDate.timeInIso8601Format()
		docValue.userUpdated = KSDate.timeInIso8601Format()
	end

	if not docValue.importSource then
		docValue.importSource = {}
	end
	
	if not docValue._localOnly then
		docValue._localOnly = {}
	end

	--Android specific comment
	--be careful using original, for OS < AndroidQ original will have absolute path
	--for OS >= AndroidQ original will have relative path to file.
	local localUrl = derefOpt( docValue, '_localOnly', 'localUrl' )

	local isFileUrl = localUrl and string.find( localUrl, 'file://' ) == 1 or false

	if isFileUrl then
		docValue._localOnly.localUrl = nil
	end
	
	local isVideo = docValue.video and true or false
	
	docValue.subtype = isVideo and 'video' or 'image'
	docValue.importSource[ 'type' ] = nil
	docValue.importSource[ 'importedOnDevice' ] = string.format( '%s {%s}', WFDeviceInfo.deviceName(), deviceId )
	docValue.importSource[ 'importedBy' ] = userId
	
	if not derefOpt( docValue, 'importSource', 'importTimestamp' ) then
		docValue.importSource[ 'importTimestamp' ] = KSDate.timeInIso8601Format( nil, false )
	end

	docValue.importSource[ 'localAssetId' ] = localUrl


	docValue.importSource[ 'uniqueDeviceId' ] = WFDeviceInfo.uniqueDeviceId()
	
	if docValue.height then
		docValue.importSource[ 'originalHeight' ] = docValue.height
	end
	
	if docValue.width then
		docValue.importSource[ 'originalWidth' ] = docValue.width
	end

	local exif = derefOpt ( docValue, "xmp", "exif" )
	if exif then

		if exif.ApertureValue and #exif.ApertureValue < 2 then
			-- Invalid Aperature Value, remove it
			docValue.xmp.exif.ApertureValue = nil
		end

		if exif.FNumber and #exif.FNumber < 2 then
			-- Invalid FNumber, remove it
			docValue.xmp.exif.FNumber = nil
		end

		if exif.FocalLength and #exif.FocalLength < 2 then
			-- Invalid Focal length, remove it
			docValue.xmp.exif.FocalLength = nil
		end

		if exif.ExposureTime and #exif.ExposureTime < 2 then
			-- Invalid exposure time, remove it
			docValue.xmp.exif.ExposureTime = nil
		end

		if exif.ShutterSpeedValue and #exif.ShutterSpeedValue < 2 then
			docValue.xmp.exif.ShutterSpeedValue = nil
		end

		if exif.ExposureBiasValue and #exif.ExposureBiasValue < 2 then
			docValue.xmp.exif.ExposureBiasValue = nil
		end

		if exif.MaxApertureValue and #exif.MaxApertureValue < 2 then
			docValue.xmp.exif.MaxApertureValue = nil
		end

		if exif.BrightnessValue and #exif.BrightnessValue < 2 then
			docValue.xmp.exif.BrightnessValue = nil
        end

		if exif.SubjectDistance and #exif.SubjectDistance < 2 then
			docValue.xmp.exif.SubjectDistance = nil
		end

		if exif.ApproximateFocusDistance and #exif.ApproximateFocusDistance < 2 then
			docValue.xmp.aux.ApproximateFocusDistance = nil
		end

	end

	--exportImpLog:debugf( "tarun_import ExposureProgram in lua is %s",docValue.xmp)

	docValue.height = nil
	docValue.width = nil
	docValue.originalCaptureDate = nil
	
	local docStore = catalog:docStore()
	
	local cacheEpochB = OzEpoch_cacheEpochB( catalog )
	local epochNow = cacheEpochB:valueNow()
	cacheEpochB:close()
	
	local newRevId = asset.revId or KSIdGenerator.uuidInLowercaseHex()
	local assetDocId = asset.assetId or KSIdGenerator.uuidInLowercaseHex()

	return LrmImport.hasAssetBySha256P( catalog, sha256, bypassSha256Check, userId )
	
		:mapDataP_new( function( assetId )
		
			if assetId then
			
				return createImportResult( assetId, localUrl, kImportStatusDuplicateAsset )
				
			end
			
			return catalog:getOriginalsDocumentsFolderP()

				:mapDataP_new( function( documentsPath )

					return saveOriginalP( catalog, asset, epochNow, documentsPath, dontSave )

						:mapDataP_new( saveProxyP( catalog, asset, epochNow, documentsPath, dontSave ) )

				end )

				:mapDataP_new( function()

					if dontSave then
						return KSPromises.resolvedPromiseP( 'data', asset )
					end

					return catalog:getDataCacheP()

						:mapDataP_new ( function( dataCache )

							return saveRenditionSetP( catalog, asset, epochNow, dataCache, newRevId, assetDocId )

						end )

				end )
				
				:mapDataP_new( function( asset )
									
					
					docValue._newRevId = newRevId
					
					return docStore:applyOverlayToDocumentP {
						docId = assetDocId,
						docType = 'asset',
						overlay = docValue,
					}
					:mapDataP_new( function()
					
						return createImportResult( assetDocId, localUrl, kImportStatusNewAsset )
						
					end )
					
				end )
		
		end )
	
		:mapP( function( ... )
		
			local originalData = asset.original
			
			if originalData and KSData.isData( originalData ) then
				originalData:close()
			end
			
			local proxyData = asset.proxy
			
			if proxyData and KSData.isData( proxyData ) then
				proxyData:close()
			end
			
			return ...
			
		end )
		
		:runUntilDoneP()
		
end

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

local function getDataCachePathP( catalog )
	
	return catalog:getDataCachePathP()
	
end

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

function LrmImport.addAssetArrayToCatalogP( catalog, assetArray, options )

--	check( 'OzCatalogServiceImp', 'table', 'table' )
	
	local minimumFreeDiskSpace = assert( options.minimumFreeDiskSpace )
	
	local function _addAssetToCatalogP( asset )
	
		if type( asset.docValue ) == 'string' then
		
			local result, docValue = pcall( KSJsonDecoder.decodeWebSafeJson, asset.docValue )
			
			asset.docValue = nil
			
			if result then
				
				asset.docValue = docValue
				
			end
			
		end
		
		if not asset.docValue or type( asset.docValue ) ~= 'table' then
			
			return KSPromises.resolvedPromiseP( 'error', 'No valid docValue for asset' )
			
		end
			
		local derivedFrom = asset.derivedFrom
		
		-- INFO [pkamin 2015-08-31]: we don't verify the revisionId as we might not have a revision
		-- due to not yet uploaded metadata
		
		if derivedFrom and derivedFrom.catalogId and derivedFrom.assetId then
		
			return catalog:getCatalogIdP()
			
				:mapDataP_new( function( catalogId )
			
					if catalogId == derivedFrom.catalogId then
					
						return catalog:docStore():docValueOnceP( 'asset', derivedFrom.assetId )
							
							:mapP( function( status, originDocValue )
							
								local newDocValue = asset.docValue
								
								if status == 'data' and originDocValue._changeCounter then
								
									local derivedFromInstance = derivedFrom.revisionId 
													and string.format( '/v2/catalogs/%s/assets/%s/revisions/%s', 
																		catalogId, 
																		originDocValue._docId, 
																		derivedFrom.revisionId ) 
													or nil
									
									local derivedFromDocument = string.format( '/v2/catalogs/%s/assets/%s', 
																	catalogId, 
																	originDocValue._docId )
									
									--------------------------------------------------------------------------------
									
									local derivedOverlay = {
										xmp = {
											xmpMM = {
												DerivedFromDocument = derivedFromDocument,
												DerivedFromInstance = derivedFromInstance,
											}
										}
									}
									
									local originXmp = derefOpt( originDocValue, 'xmp' )
									
									local copiedXmp = {
										xmp = originXmp,
									}
										
									WFTableMerge.applyOverlay( newDocValue, copiedXmp )
									
									WFTableMerge.applyOverlay( newDocValue, derivedOverlay )
									
								end
								
								asset.docValue = newDocValue
								
								return 'data', asset
							
							end )
							
							:mapDataP_new( function()
								return catalog:ozUserIdP()
							end )
							
							:mapDataP_new( function( userId )
								return addAssetToCatalogP( catalog, asset, userId, options )
							end )
						
					end
					
					return catalog:ozUserIdP()
							
						:mapDataP_new( function( userId )
							return addAssetToCatalogP( catalog, asset, userId, options )
						end )
					
				end )
			
		end
		
		return catalog:ozUserIdP()
			
			:mapDataP_new( function( userId )
			
				return addAssetToCatalogP( catalog, asset, userId, options )
				
			end )
		
	end
	
	return getDataCachePathP( catalog )
		
		:mapDataP_new( function( dataCachePath )
			local fileSystemFreeSpace = 0
			local WFPlatformInfo = require 'WFPlatformInfo'

			if WFPlatformInfo.isAndroid then
				local dataCacheDir = KSPathUtils.parent( dataCachePath )
				fileSystemFreeSpace = WFFileUtils.freeSpaceAtPath( dataCacheDir )
			else
				fileSystemFreeSpace = WFFileUtils.freeSpaceAtPath( dataCachePath )
			end


			local spaceNeeded = minimumFreeDiskSpace - fileSystemFreeSpace
		
			if spaceNeeded > 0 then
				return 'error', 'LowDiskSpace'
			end
			
			return 'data', false
	
		end )
		
		:mapDataP_new( function()
		
			return KSPromises.mapArrayP( _addAssetToCatalogP, assetArray, 5 )
				:mapP( function( status, data )
				
					return status, data
					
				end )
	
		end )
		
end



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

function LrmImport.hasAssetBySha256P( catalog, sha256, bypassSha256Check, userId )

	if bypassSha256Check then
		return KSPromises.resolvedPromiseP( 'data', nil )
	end

	if not( type( userId ) == 'string' and #userId == 32 ) then
		error( "hasAssetBySha256P: userId must be a 32-byte guid", 2 )
	end

	if not( type( sha256 ) == 'string' and #sha256 == 64 ) then
		error( "hasAssetBySha256P: sha256 must be a 64-byte hex string", 2 )
	end
	
	local sql = [[ SELECT docId FROM coreInfo 
					WHERE sha256 = ? 
					AND importedBy = ?
					AND spaceID IS NULL ]] -- Only consider catalog-based assets.
					
	local params = { sha256, userId }

	return catalog:indexStore():queryP( sql, unpack( params ) )
		:mapDataP_new( function( row )

		return row and row[ 1 ] and row[ 1 ][ 1 ]
			
		end )

end



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

return LrmImport
