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

WFCatVersions.lua

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

ADOBE CONFIDENTIAL
------------------
Copyright 2020 Adobe
All Rights Reserved.

NOTICE: All information contained herein is, and remains
the property of Adobe and its suppliers, if any. The intellectual
and technical concepts contained herein are proprietary to Adobe
and its suppliers and are protected by all applicable intellectual
property laws, including trade secret and copyright laws.
Dissemination of this information or reproduction of this material
is strictly forbidden unless prior written permission is obtained
from Adobe.

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

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

	-- WF.core.utils
local KSDate = require 'KSDate'
local KSIdGenerator = require 'KSIdGenerator'
local KSStringUtils = require 'KSStringUtils'
local KSTableUtils = require 'KSTableUtils'
local WFArguments = require 'WFArguments'
local WFLogger = require 'WFLogger'
local WFPlatformInfo = require 'WFPlatformInfo'
local WFRxRunUntilDone = require 'WFRxRunUntilDone'
local WFTableMerge = require 'WFTableMerge'

	-- WF.oz_client.managed_catalog
local WFCatAssetAux = require 'WFCatAssetAux'
local WFCatAssetUtils = require 'WFCatAssetUtils'
local WFCatCollaboration = require 'WFCatCollaboration'
local WFCatCopies = require 'WFCatCopies'
local WFCatDelete = require 'WFCatDelete'
local WFCatIndexUtils = require 'WFCatIndexUtils'
local WFCatOzErrors = require 'WFCatOzErrors'
local WFCatTrashUtils = require 'WFCatTrashUtils'
local WFCatUtils = require 'WFCatUtils'

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

local logger = WFLogger( 'WFCatVersions' )

local WFArguments_check = assert( WFArguments.check )

local KSTableUtils_derefOpt = assert( KSTableUtils.derefOpt )

local KSTableUtils_setNested = assert( KSTableUtils.setNested )

local WFCatAssetUtils_getLocalExternalDevelopSettingsPath = assert( WFCatAssetUtils.getLocalExternalDevelopSettingsPath )

local WFCatIndexUtils_getOrientationWidthAndHeight = assert( WFCatIndexUtils.getOrientationWidthAndHeight )

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

local OZ_MAX_VERSION_NAME_CHARS = 250
local kVersionSubtypeEdit = 'edit'
local kSpaceEditor = 'editor'

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

local function intToBool( zeroOrOne )
	return zeroOrOne ~= 0
end

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

-- @mark WFCatVersions namespace

local WFCatVersions = {}

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

function WFCatVersions.generateIndexSchema( indexStoreSchema )

	WFArguments_check( 'WFRxIndexStoreSchema' )

	indexStoreSchema:addTable {

		sql = [[ CREATE TABLE versions(
					localId,
					assetId,
					creationDate,
					name, 
					subtype, 
					importedByUser,
					importedByFormFactor,
					originalSha256,
					embeddedDevelopSettings,
					hasExternalizedDevelopSettings,
					externalXmpRelativePath,
					croppedWidth, 
					croppedHeight, 
					userOrientation,
					createdManually,
					spaceId,
					incomplete,
					linkedLocationId,
					linkedLocationSource,
					linkedLocationSha
				) ]],
				
		relevantTypeSubtypeTuples = {
			{ 'version', '*' },
		},

		hasOneRowPerDocument = true,
		
		map = function( docValue, emit )

			if not docValue._deleted then
			
				local localId = docValue._localId
			
				local spaceId = docValue._spaceId
			
				-- SPAM [jandrew 2020-04-20]: https://jira.corp.adobe.com/browse/LRMA-4199993
				-- When a Version documention is downloaded from Oz, we have special handling 
				-- inside of WFOzDocUtils.remapBasicOzDocument which moves the Asset section 
				-- of the document. This is the same remapping that is done for comments, favorites, 
				-- etc. If an older client downloads a Version document created by a new client,
				-- it's not remapped. So then when the older client upgrades to a Version-doc aware build,
				-- the previously synced local Versions docs still have their Asset sections under
				-- _serverProvidedMetadata. There is a similar issue for the formFactor
				
				local assetSubsection = KSTableUtils_derefOpt( docValue, 'asset' ) 
						or KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'asset' )
				local importedByFormFactor = KSTableUtils_derefOpt( docValue, 'importedOnDevice', 'formFactor' )
						or KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'importedOnDevice', 'formFactor' )
				
				local assetId = KSTableUtils_derefOpt( assetSubsection, 'id' )
				local originalSha256 = KSTableUtils_derefOpt( assetSubsection, 'sha256' )
			
				local creationDate = KSTableUtils_derefOpt( docValue, 'userCreated' )
				local versionName = KSTableUtils_derefOpt( docValue, 'name' )
				local versionType = docValue.subtype

				local importedByUser = KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'importedByUser', 'id' )
				
				 
				local developSection = KSTableUtils_derefOpt( docValue, 'develop' )
				
				local xmpCameraRaw = KSTableUtils_derefOpt( developSection, 'xmpCameraRaw' )
				local croppedWidth = KSTableUtils_derefOpt( developSection, 'croppedWidth' )
				local croppedHeight = KSTableUtils_derefOpt( developSection, 'croppedHeight' )
				local userOrientation = KSTableUtils_derefOpt( developSection, 'userOrientation' )
                                
				local linkedLocationId = KSTableUtils_derefOpt( docValue, 'linkedLocation', 'id' )
				local linkedLocationSource = KSTableUtils_derefOpt( docValue, 'linkedLocation', 'source' )

				-- Note that this column will only be valuable on Android devices
				local linkedLocationSha = KSTableUtils_derefOpt( docValue, 'linkedLocation', 'sha256' )

				local isExternalized = type( xmpCameraRaw ) == 'table'
				
				local hasXmpDevelopUrl = not not KSTableUtils_derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/xmp/develop', 'href' ) 

				local embeddedDevelopSettings
				
				if not isExternalized then
					embeddedDevelopSettings = xmpCameraRaw
				end
				
				local externalXmpRelativePath = WFCatAssetUtils_getLocalExternalDevelopSettingsPath( docValue )
					-- This value is only present for versions with externalized develop XMP, and only applies
					-- to Android & iOS
					
				local createdManually = KSTableUtils_derefOpt( docValue, 'createdManually' ) and 1 or 0
					
				local incomplete = isExternalized and not externalXmpRelativePath and not hasXmpDevelopUrl
					-- If the settings have been externalized, and we have no external settings locally and
					-- Oz doesn't have them either, then the settings have yet to be uploaded.

				if WFCatOzErrors.hasReferencedAuxMissingError( docValue ) then
					-- This error occurs when the parent Asset document does not contain Aux entries that
					-- this Version requires.
					incomplete = true
				end

				incomplete = incomplete and 1 or 0

				isExternalized = isExternalized and 1 or 0
					
				emit( localId, assetId, creationDate, versionName, versionType, importedByUser, importedByFormFactor,
						originalSha256, embeddedDevelopSettings, isExternalized, externalXmpRelativePath, croppedWidth, 
						croppedHeight, userOrientation, createdManually, spaceId, incomplete, linkedLocationId, 
						linkedLocationSource, linkedLocationSha )
						
			end
								
		end

	}
	
	indexStoreSchema:addIndex [[CREATE INDEX versions_assetId ON versions( assetId )]]

	indexStoreSchema:addIndex [[CREATE INDEX versions_spaceId ON versions( spaceId )]]

	indexStoreSchema:addIndex [[CREATE INDEX versions_localId ON versions( localId )]]

	indexStoreSchema:addIndex [[CREATE INDEX versions_incomplete ON versions( incomplete )]]

	indexStoreSchema:addIndex [[CREATE INDEX versions_linkedLocationId ON versions( linkedLocationId )]]

end

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

local function maybeMakeVersionNameValid( versionName )

	if KSStringUtils.lengthOfUTF8String( versionName ) > OZ_MAX_VERSION_NAME_CHARS then
	
		logger:warnf( 'Version names are limited to %d chars, truncating: %s', OZ_MAX_VERSION_NAME_CHARS, versionName )
		versionName = KSStringUtils.UTF8Substring( versionName, 1, OZ_MAX_VERSION_NAME_CHARS )
		
	end
	
	return versionName

end

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

local function checkUserAuthorization( catalog, docId )

	return catalog:docStore():docValue( docId )
		
		:flatMap( function( docValue )
			
			if not docValue._spaceId then
			
				return Observable.just( true )
			
			else
			
				return catalog:indexStore():queryOnce( 
					{ 
						sql = [[SELECT role
									FROM space
									INNER JOIN space_user ON space_user.fullDocId == space.spaceUserId
									WHERE space.docId == ?]],
						quiesceUntilUpToDate = true,
						docValue._spaceId
					} )
					
					:map( function( rows )
					
						local role = KSTableUtils_derefOpt( rows, 1, 1 ) or ""
						
						role = WFCatCollaboration.downgradeRoleIfNotSupported( role )
						
						if role == kSpaceEditor then
							return true
						end
							
						return error( string.format( "User doesn't have authorized role for space %s, with role %s",
									docValue._spaceId, role ) )
						
					end )
			
			end
			
		end )

end

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

local function returnTrue()
	return true
end

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

local function copyDevelopSettingsReferencesAndAddVersionToDocStore( catalog, sourceDocValue, versionDocValue )

	local versionId = versionDocValue._id

	local copyOptions = {
		needToCopyOriginal = false,
		needToCopyProxy = false,
		needToCopyRenditions = false,
		needToCopyExternalDevelopSettings = true,
		needToCopyAux = true,
		needToCopyXmpSidecar = false
	}
		
	return Observable.just( copyOptions )
	
		:map( function( copyOptions )
		
			-- The following API call copies the external XMP develop settings from the
			-- existing asset document to the versions document.
					
			return WFCatCopies.calculateCopyPathsAndUpdateDocValue( catalog, sourceDocValue, versionDocValue, copyOptions )
		
		end )
		
		:flatMap( function( modifiedVersionDocValue, filesToCopy )
		
			-- On Desktop, filesToCopy should always be an empty table. WFCatCopies.calculateCopyPathsAndUpdateDocValue
			-- is platform-aware and knows that xmp settings are shared between documents on Desktop.
		
			-- If necessary, copy the external xmp to the new location.
			
			local maybeCopyExternalXmpFile = Observable.just( true )
		
			if #filesToCopy > 0 then
			
				-- Note, this block is mobile only.
				
				local xmpFileInfo = filesToCopy[ 1 ]
				
				local externalXmpPath = xmpFileInfo.srcFilePath
				local externalXmpCopyPath = xmpFileInfo.dstFilePath
				
				if externalXmpPath and externalXmpCopyPath then
					maybeCopyExternalXmpFile = WFCatCopies.copyExternalXmpFile( externalXmpPath, externalXmpCopyPath )
				end
				
			end
		
			return maybeCopyExternalXmpFile
		
				:flatMap( function()
				
					return catalog:docStore():setDocValue( modifiedVersionDocValue )
					
				end )
				
		end )
		
		:catch( function( errorMsg )
		
			logger:warnf( "Error copying version XMP: %s", errorMsg )
		
			return catalog:docStore():setDocValue( versionDocValue )
		
		end )
		
		:map( function()
		
			return versionId
			
		end )

end

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

local function truncateCroppedWidthAndHeightSettingsForVersion( versionDoc )

	-- WF-1363: Oz has reported a non trivial number of Version document uploads 
	-- that contain invalid croppedWidth/croppedHeight parameters. These values
	-- are being supplied as floats, when only integers are allowed. This results
	-- in `PayloadValidationError` errors. The following code converts the fields
	-- to integers, and deletes the field entirely as a fall back.

	-- croppedWidth
	
	local croppedWidth = KSTableUtils_derefOpt( versionDoc, "develop", "croppedWidth" )
	
	if type( croppedWidth ) == 'number' then
		croppedWidth = math.floor( croppedWidth )
	else
		croppedWidth = nil
	end
	
	KSTableUtils_setNested( versionDoc, croppedWidth, 'develop', 'croppedWidth' )

	-- croppedHeight
	
	local croppedHeight = KSTableUtils_derefOpt( versionDoc, "develop", "croppedHeight" )

	if type( croppedHeight ) == 'number' then
		croppedHeight = math.floor( croppedHeight )
	else
		croppedHeight = nil
	end
	
	KSTableUtils_setNested( versionDoc, croppedHeight, 'develop', 'croppedHeight' )
	
	return versionDoc
	
end

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

local function maybeAddOzErrorOverlay( sourceDocValue, versionDocValue )

	-- WF-1644: If the source Asset document is marked with an error preventing
	-- it's upload, then mark the Version document with the same error; there's
	-- no point in attempting to upload a Version that references an unsynced
	-- Asset document.
	
	local shouldSkipUpload, reasonToSkipUpload =
			WFCatOzErrors.shouldSkipUploadDueToError( sourceDocValue )
	
	if shouldSkipUpload then
	
		versionDocValue = WFCatOzErrors.addOverlayForOzErrorSubtype(
			versionDocValue, reasonToSkipUpload )
	
	end

end

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

local function logErrorIfVideo( subtype, funcName )

	if subtype == 'video' or subtype == 'deleted_video' then

		-- Oz doesn't want to support versions being created for video assets
		-- it does allow creation but doesn't do anything with them and doesn't
		-- want to waste the space/processing on them.  See NMB-26721

		-- For now just log an error to avoid breaking any client workflows if
		-- they accidentally use them.

		-- LrDesktop has made fixes to workflows like reset settings, paste
		-- settings, creating initial version to avoid creating versions on
		-- video.

		-- Generate an error so analytics will expose any future or unknown
		-- creation of the version documents. It's possible we may want to
		-- supoort versions for video in the future.

		logger:errorf( "Generating version document for video via %s", funcName )

	end

end

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

--[[!
	\brief Create a new version based on the current assets' develop settings.

	\param catalog (WFCatalog)
	\param assetId (string|number) The local or full doc ID of an asset.
	\param createdManually (boolean) Represents if the version was explicitly created
		by the user, or if it was automatically created.
	\param versionName (optional string) A name for the version. If not provided,
		the name will be interpreted from the creation date. Cannot be an empty string.

	\param optLinkedLocation (table) if present, the version
	document will be annotated with linked location details. Used
	by BYOCR import and export workflows
		\key id (string) - the unique cloud identifier for the binary associated with this version
		\key source (string) - the name of the cloud provider associated with this id
		\key sha256 (optional string) - The sha256 of the associated binary (Android only)

	\return <code>Observable</code> Resolves to the version ID of the created Version doc
		on success, or throws on error when user is not authorized to create version.
--]]

function WFCatVersions.createVersionForAsset( catalog, assetId, createdManually, versionName, optLinkedLocation )

	WFArguments_check( "WFCatalog", "+string|number", 'boolean', '?string', '?table' )

	return checkUserAuthorization( catalog, assetId )

		:flatMap( function()

			logger:infof( "Creating a new version for asset %s named %s", assetId, versionName )

			local docStore = catalog:docStore()

			local versionId = KSIdGenerator.uuidInLowercaseHex()

			local baseDocValue = WFCatUtils.createBaseOverlayWithUserCreatedFields( versionId )

			if versionName == '' then
				versionName = nil
			end

			local formFactor = WFPlatformInfo.getAppDeviceClass()

			local versionDocValue = {

				type = 'version',
				subtype = 'edit',
				name = versionName,
				createdManually = createdManually,
				importedOnDevice = {
					formFactor = formFactor
				}

			}

			versionDocValue = WFTableMerge.applyOverlay( versionDocValue, baseDocValue )

			return WFCatTrashUtils.getTrashOrRestoredDocFromNewId( docStore, assetId )
				-- This guard against an Android Race condition: https://jira.corp.adobe.com/browse/LRMA-4199669
				-- In case the asset was trashed quickly after editing, make sure we're creating the version
				-- against the correct trashed asset.

				:flatMap( function( sourceDocValue )

					logErrorIfVideo( sourceDocValue.subtype, "createVersionForAsset" )

					versionDocValue.asset = {
						id = sourceDocValue._id
					}

					local sha256 = KSTableUtils_derefOpt( sourceDocValue, 'importSource', 'sha256' )

					if not sha256 then
						error( "The provided asset does not have a sha256 in the importSource." )
					end

					versionDocValue.asset.sha256 = sha256

					local developSettings = KSTableUtils_derefOpt( sourceDocValue, 'develop' )

					if not developSettings then
						error( "The provided asset does not have a develop payload." )
					end

					-- WF-1640 Nimbus is sending develop.profiles as {} in the payload
					if type( developSettings.profiles ) == 'table' and not next( developSettings.profiles ) then
						developSettings.profiles = nil
					end

					if optLinkedLocation and next( optLinkedLocation ) then

						-- For all other cases, assume that the client will provide the correct data
						versionDocValue.linkedLocation = optLinkedLocation

					end

					versionDocValue._spaceId = sourceDocValue._spaceId

					versionDocValue.develop = developSettings

					versionDocValue = truncateCroppedWidthAndHeightSettingsForVersion( versionDocValue )

					maybeAddOzErrorOverlay( sourceDocValue, versionDocValue )

					return copyDevelopSettingsReferencesAndAddVersionToDocStore( catalog, sourceDocValue, versionDocValue )

				end )

		end )

		:call( WFRxRunUntilDone )

end

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

--[[!
	\brief Create a new version based on an existing version.
	
	\param catalog (WFCatalog)
	\param versionId (string|number) The local or full doc ID of the version to duplicate.
	\param createdManually (boolean) Represents if the version was explicitly created
		by the user, or if it was automatically created.
	\param versionName (optional string) A name for the version. If not provided,
		the name will be interpreted from the creation date.
	
	\return <code>Observable</code> Resolves to the version ID of the created Version doc
		on success, or throws on error when user is not authorized to create version.
--]]

function WFCatVersions.duplicateVersion( catalog, sourceVersionId, createdManually, versionName )

	WFArguments_check( "WFCatalog", "+string|number", 'boolean', '?string' )
	
	return checkUserAuthorization( catalog, sourceVersionId )
	
		:flatMap( function()
	
			logger:infof( "Creating a new version based on version %s named %s", sourceVersionId, versionName )
			
			local docStore = catalog:docStore()
			
			local versionId = KSIdGenerator.uuidInLowercaseHex()
			
			local baseDocValue = WFCatUtils.createBaseOverlayWithUserCreatedFields( versionId )
			
			local formFactor = WFPlatformInfo.getAppDeviceClass()
			
			local versionDocValue = {
		
				type = 'version',
				subtype = 'edit',
				name = versionName,
				createdManually = createdManually,
				importedOnDevice = {
					formFactor = formFactor
				}
				
			} 
			
			versionDocValue = WFTableMerge.applyOverlay( versionDocValue, baseDocValue )
			
			return docStore:docValue( sourceVersionId )
			
				:flatMap( function( sourceDocValue )
				
					local assetSubsection = KSTableUtils_derefOpt( sourceDocValue, 'asset' ) 
							or KSTableUtils_derefOpt( sourceDocValue, '_serverProvidedMetadata', 'asset' )
				
					versionDocValue.asset = assetSubsection
					versionDocValue._spaceId = sourceDocValue._spaceId
					versionDocValue.develop = sourceDocValue.develop


					local linkedLocation = KSTableUtils_derefOpt( sourceDocValue, 'linkedLocation')

					if linkedLocation then

						versionDocValue.linkedLocation = linkedLocation

					end

					versionDocValue = truncateCroppedWidthAndHeightSettingsForVersion( versionDocValue )
					
					maybeAddOzErrorOverlay( sourceDocValue, versionDocValue )
				
					return copyDevelopSettingsReferencesAndAddVersionToDocStore( catalog, sourceDocValue, versionDocValue )
					
				end )
		
		end )

		:call( WFRxRunUntilDone )

end

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

local function applyDevelopSettingsOverridesToVersionDoc( versionDocValue, overrides )

	if overrides.croppedWidth then

		logger:infof( "Overriding cropped width for Version: %d", overrides.croppedWidth )

		KSTableUtils_setNested( 
				versionDocValue, overrides.croppedWidth, 'develop', 'croppedWidth' )

	end

	if overrides.croppedHeight then

		logger:infof( "Overriding cropped height for Version: %d", overrides.croppedHeight )

		KSTableUtils_setNested( 
				versionDocValue, overrides.croppedHeight, 'develop', 'croppedHeight' )

	end

	if overrides.userOrientation then

		logger:infof( "Overriding user orientation for Version: %d", overrides.userOrientation )

		KSTableUtils_setNested( 
				versionDocValue, overrides.userOrientation, 'develop', 'userOrientation' )

	end

	if overrides.cameraProfileHref and overrides.cameraProfileFilename then

		logger:infof( "Overriding camera profile for Version: %s, %s", 
				overrides.cameraProfileHref, overrides.cameraProfileFilename )

		KSTableUtils_setNested( 
				versionDocValue, overrides.cameraProfileHref, 'develop', 'profiles', 'camera', 'href' )

		KSTableUtils_setNested( 
				versionDocValue, overrides.cameraProfileFilename, 'develop', 'profiles', 'camera', 'filename' )

	end

	if overrides.lensProfileHref and overrides.lensProfileFilename then

		logger:infof( "Overriding lens profile for Version: %s, %s", 
				overrides.lensProfileHref, overrides.lensProfileFilename )

		KSTableUtils_setNested( 
				versionDocValue, overrides.lensProfileHref, 'develop', 'profiles', 'lens', 'href' )

		KSTableUtils_setNested( 
				versionDocValue, overrides.lensProfileFilename, 'develop', 'profiles', 'lens', 'filename' )

	end

	return versionDocValue

end

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

local function doesDocIdAlreadyExist( docStore, docId )

	-- Check if the doc Id already exists in the doc store using docIdToLocalId.
	-- docIdToLocalId will return the local doc id for this document or nil
	-- if the full doc id does not exist within the doc store.

	return docStore:docIdToLocalId( docId )

		:map( function( maybeLocalId )

			return not not maybeLocalId

		end )

end

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

-- Function to check if all items in auxDigest exist in parentAssetAux
local function areAuxDigestsInParentAssetAux( auxDigests, parentAssetAuxDigests )

    auxDigests = auxDigests or {}
	
    parentAssetAuxDigests = parentAssetAuxDigests or {}

    -- Check if all auxDigests are keys in parentAssetAuxDigests
    for auxId in pairs( auxDigests ) do
        if not parentAssetAuxDigests[ auxId ] then
            return false
        end
    end

    return true
	
end

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

--[[!
	\brief Create a new version of a given asset using the provided INLINE XMP 
		develop settings.
	
	\param catalog (WFCatalog)
	\param assetId (string|number) The local or full doc ID of an asset.
	\param createdManually (boolean) Represents if the version was explicitly created
		by the user, or if it was automatically created.
	\param versionName (optional string) A name for the version. If not provided,
		the name will be interpreted from the creation date.
	\param developSettings (table) A table of develop settings used to create the
		version document. Any optional settings not provided will assume the values
		from the associated asset document:
		
			cameraRawXmpStr = < string >
			
			croppedWidth = < optional number >
			croppedHeight = < optional number >
			userOrientation = < optional number >
			cameraProfileHref = < optional string >
			cameraProfileFilename = < optional string >
			lensProfileHref = < optional string > 
			lensProfileFilename = < optional string >
	
			_id = < optional string > id of the version document to be created

	\param optLinkedLocation (table) if present, the version
	document will be annotated with linked location details. Used
	by BYOCR import and export workflows
		\key id (string) - the unique cloud identifier for the binary associated with this version
		\key source (string) - the name of the cloud provider associated with this id
		\key sha256 (optional string) - The sha256 of the associated binary (Android only)

	\return <code>Observable</code> The version ID of the created Version doc. This function will
		return an error if the developSettings do not contain the appropriate camera raw xmp
		string, if the string is too long, or if the source asset is missing a develop payload.
--]]

function WFCatVersions.createVersionForAssetWithSettings( catalog, assetId, createdManually, versionName, developSettings, optLinkedLocation )

	WFArguments_check( "WFCatalog", "+string|number", 'boolean', '?string', 'table' )
	
	logger:infof( "Creating a new version for asset %s named %s", assetId, versionName )
	
	local docStore = catalog:docStore()
	
	-- Verify the presence of the inline XMP
	
	local cameraRawXmpStr = developSettings.cameraRawXmpStr
	
	if not cameraRawXmpStr then
		return Observable.throw( "cameraRawXmpStr was not provided in the develop settings." )
	end
	
	-- Verify the inline XMP is not over 10kb.
	
	local settingsLength = string.len( cameraRawXmpStr )

	local INLINE_XMP_CAMERA_RAW_MAX_LENGTH = 10 * 1024 --10KB

	if settingsLength > INLINE_XMP_CAMERA_RAW_MAX_LENGTH then
		return Observable.throw( "Inline XMP settings length exceeded. Please use createVersionForAssetWithExternalSettings." )
	end
	
	local doesVersionIdAlreadyExistStream = developSettings._id
			and doesDocIdAlreadyExist( docStore, developSettings._id )
			or Observable.just( false )

	return doesVersionIdAlreadyExistStream

		:flatMap( function( doesVersionIdAlreadyExist )

			if doesVersionIdAlreadyExist then
				return Observable.throw( "The supplied version document ID already exists within the doc store." )
			end

			return WFCatTrashUtils.getTrashOrRestoredDocFromNewId( docStore, assetId )

		end )

		:flatMap( function( sourceDocValue )

			logErrorIfVideo( sourceDocValue.subtype, "createVersionForAssetWithSettings" )

			-- Create the Version Document

			local versionId = developSettings._id or KSIdGenerator.uuidInLowercaseHex()

			local baseDocValue = WFCatUtils.createBaseOverlayWithUserCreatedFields( versionId )

			local formFactor = WFPlatformInfo.getAppDeviceClass()

			local versionDocValue = {

				type = 'version',
				subtype = 'edit',
				name = versionName,
				createdManually = createdManually,
				importedOnDevice = {
					formFactor = formFactor
				}

			}

			versionDocValue = WFTableMerge.applyOverlay( versionDocValue, baseDocValue )

			-- Further populate the Version document with info from the asset.
			
			versionDocValue.asset = {
				id = sourceDocValue._id
			}
		
			versionDocValue._spaceId = sourceDocValue._spaceId
		
			local sha256 = KSTableUtils_derefOpt( sourceDocValue, 'importSource', 'sha256' )
			
			if not sha256 then
				error( "The provided asset does not have sha256 in the importSource." )
			end
			
			versionDocValue.asset.sha256 = sha256
			
			-- Use the develop settings from the asset as the base settings..
			
			local assetDevelopSettings = KSTableUtils_derefOpt( sourceDocValue, 'develop' )
		
			if not assetDevelopSettings then
				error( "The provided asset does not have a develop payload." )
			end
		
			versionDocValue.develop = assetDevelopSettings
			versionDocValue.develop.masks = nil
			versionDocValue.develop.filters = nil
				-- Masks are associated with the current asset's develop settings, and therefore
				-- we must delete them as they are not valid for the Version document that
				-- is being created. If a client wants to explicitly create a Version that
				-- has masks, they must use the createVersionForAssetWithExternalSettings API
				-- and provide the auxInfo field within the developSettings parameter.

			-- .. Then override them with the user supplied settings. Any field not provided by the caller
			-- will maintain the value from the asset document.

			if optLinkedLocation and next( optLinkedLocation ) then

				versionDocValue.linkedLocation = optLinkedLocation

			end

			versionDocValue = applyDevelopSettingsOverridesToVersionDoc( versionDocValue, developSettings )
		
			versionDocValue = truncateCroppedWidthAndHeightSettingsForVersion( versionDocValue )
		
			KSTableUtils_setNested( 
					versionDocValue, developSettings.cameraRawXmpStr, 'develop', 'xmpCameraRaw' )
		
			maybeAddOzErrorOverlay( sourceDocValue, versionDocValue )

			-- Check auxiliaries digests are tracked by the parent asset
			local versionAuxDigest = WFCatAssetAux.extractDevelopAuxDigestsFromDocValue( versionDocValue )

			local parentAssetAuxDigests = sourceDocValue.aux

			if not areAuxDigestsInParentAssetAux( versionAuxDigest, parentAssetAuxDigests ) then
				return Observable.throw( 'Version auxiliaries are not referenced in the parent asset document.' )
			end
		
			return docStore:setDocValue( versionDocValue )
		
				:map( function()
		
					return versionId
					
				end )
		
		end )
		
		:call( WFRxRunUntilDone )

end

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

--[[!
	\brief Create a new version of a given asset using externalixd develop settings.
	
	\param catalog (WFCatalog)
	\param assetId (string|number) The local or full doc ID of an asset.
	\param createdManually (boolean) Represents if the version was explicitly created
		by the user, or if it was automatically created.
	\param versionName (optional string) A name for the version. If not provided,
		the name will be interpreted from the creation date.
	\param developSettings (table) A table of develop settings used to create the
		version document. Any optional settings not provided will assume the values
		from the associated asset document:
			
			cameraRawXmpFilePath = < string >
				Please note that the file path must use the appropriate semantics
				based on the current platform. On mobile, this means the path
				must be a relative one.
			cameraRawXmpSha256 = < string >
			cameraRawXmpSize = < number >
			auxInfo = <optional table>: A table containing auxiliary object information. Format:
				{
					{
						digest = digest,
					},
					{
						digest = digest,
						groupDigest = groupDigest,
						sourceLongEdge = 6000,
						crsSizeX = 1024,
						crsSizeY = 768
					},
					{
						digest = digest,
						groupDigest = groupDigest,
						targetLongEdge = 2560,
						crsSizeX = 437,
						crsSizeY = 328
					},
					...
				}
			croppedWidth = < optional number >
			croppedHeight = < optional number >
			userOrientation = < optional number >
			cameraProfileHref = < optional string >
			cameraProfileFilename = < optional string >
			lensProfileHref = < optional string > 
			lensProfileFilename = < optional string >

			_id = < optional string > id of the version document to be created

	\param optLinkedLocation (table) if present, the version
	document will be annotated with linked location details. Used
	by BYOCR import and export workflows
		\key id (string) - the unique cloud identifier for the binary associated with this version
		\key source (string) - the name of the cloud provider associated with this id
		\key sha256 (optional string) - The sha256 of the associated binary (Android only)

	\return <code>Observable</code> The version ID of the created Version doc. This function will
		return an error if the developSettings do not contain the appropriate camera raw xmp
		data,or if the source asset is missing a develop payload and/or a sha256.
--]]

function WFCatVersions.createVersionForAssetWithExternalSettings( catalog, assetId, createdManually, versionName, developSettings, optLinkedLocation )

	WFArguments_check( "WFCatalog", "+string|number", 'boolean', '?string', 'table' )
	
	logger:infof( "Creating a new version for asset %s named %s", assetId, versionName )
	
	local docStore = catalog:docStore()
	
	-- Verify all the required fields were supplied.
	
	if not developSettings.cameraRawXmpFilePath
		or not developSettings.cameraRawXmpSha256
		or not developSettings.cameraRawXmpSize then

		return Observable.throw( "Required develop XMP fields were not provided in the develop settings." )

	end
	
	local doesVersionIdAlreadyExistStream = developSettings._id
			and doesDocIdAlreadyExist( docStore, developSettings._id )
			or Observable.just( false )

	local versionId = developSettings._id or KSIdGenerator.uuidInLowercaseHex()

	return doesVersionIdAlreadyExistStream

		:flatMap( function( doesVersionIdAlreadyExist )

			if doesVersionIdAlreadyExist then
				return Observable.throw( "The supplied version document ID already exists within the doc store." )
			end

			return WFCatTrashUtils.getTrashOrRestoredDocFromNewId( docStore, assetId )

		end )

		:flatMap( function( sourceDocValue )

			-- If developSettings.auxInfo contains resampled localOnly aux, we need to 
			-- first apply it to asset

			local auxDataList = developSettings.auxInfo or {}

			return WFCatAssetAux.getAuxOverlayForLocalOnlyResampledAux( catalog, assetId, auxDataList, sourceDocValue )
				
				:flatMap( function( auxOverlayForAsset )

					if not next( auxOverlayForAsset ) then
						return Observable.just( sourceDocValue )
					end

					auxOverlayForAsset._id = assetId

					return docStore:applyOverlay( auxOverlayForAsset )
						
						:map( function()

							sourceDocValue = WFTableMerge.applyOverlay( sourceDocValue, auxOverlayForAsset )
							
							return  sourceDocValue

						end )
					
				end )

		end )

		:flatMap( function( sourceDocValue )

			logErrorIfVideo( sourceDocValue.subtype, "createVersionForAssetWithExternalSettings" )

			local auxDataList = developSettings.auxInfo or {}
			developSettings.auxInfo = nil

			return WFCatAssetAux.getAuxOverlayForVersion( catalog, versionId, auxDataList, sourceDocValue )

				:map( function( auxOverlay )

					WFCatAssetAux.maybeTrackAuxAnalytics( #auxDataList, 'Version Creation' )

					return sourceDocValue, auxOverlay

				end )

		end )

		:flatMap( function( sourceDocValue, auxOverlay )

			-- Create the Version Document

			local baseDocValue = WFCatUtils.createBaseOverlayWithUserCreatedFields( versionId )

			local formFactor = WFPlatformInfo.getAppDeviceClass()

			local versionDocValue = {

				type = 'version',
				subtype = 'edit',
				name = versionName,
				createdManually = createdManually,
				importedOnDevice = {
					formFactor = formFactor
				}

			}

			versionDocValue = WFTableMerge.applyOverlay( versionDocValue, baseDocValue )

			-- Further populate the Version document with info from the asset.
			
			versionDocValue.asset = {
				id = sourceDocValue._id
			}
		
			versionDocValue._spaceId = sourceDocValue._spaceId
		
			local sha256 = KSTableUtils_derefOpt( sourceDocValue, 'importSource', 'sha256' )
			
			if not sha256 then
				error( "The provided asset does not have sha256 in the importSource." )
			end
			
			versionDocValue.asset.sha256 = sha256
			
			-- Use the develop settings from the asset as the base settings..
			
			local assetDevelopSettings = KSTableUtils_derefOpt( sourceDocValue, 'develop' )
		
			if not assetDevelopSettings then
				error( "The provided asset does not have a develop payload." )
			end
		
			versionDocValue.develop = assetDevelopSettings
			versionDocValue.develop.masks = nil
			versionDocValue.develop.filters = nil
			
				-- Masks and Filters are associated with the current asset's develop settings, and therefore
				-- we must delete them as they are not valid for the Version document that
				-- is being created.

			-- .. Then override them with the user supplied settings. Any field not provided by the caller
			-- will maintain the value from the asset document.

			if optLinkedLocation and next( optLinkedLocation ) then

				versionDocValue.linkedLocation = optLinkedLocation

			end

			versionDocValue = applyDevelopSettingsOverridesToVersionDoc( versionDocValue, developSettings )
		
			versionDocValue = truncateCroppedWidthAndHeightSettingsForVersion( versionDocValue )
		
			KSTableUtils_setNested( 
						versionDocValue, {}, 'develop', 'xmpCameraRaw' )
		
			KSTableUtils_setNested( 
						versionDocValue, developSettings.cameraRawXmpSha256, 'develop', 'xmpCameraRaw', 'sha256' )

			-- Add in the develop XMP localOnly fields.
			
			local sha256FieldName = WFPlatformInfo.isiOS and 'contentId' or 'sha256'

			local localOnly = { 
				files = {
					xmp_develop = {
						path = developSettings.cameraRawXmpFilePath,
						fileSize = developSettings.cameraRawXmpSize,
						[ sha256FieldName ] = developSettings.cameraRawXmpSha256,
					}
				}
			}

			versionDocValue._localOnly = localOnly
			
			maybeAddOzErrorOverlay( sourceDocValue, versionDocValue )
		
			WFTableMerge.applyOverlay( versionDocValue, auxOverlay )

			-- Check auxiliaries digests are tracked by the parent asset
			local versionAuxDigest = WFCatAssetAux.extractDevelopAuxDigestsFromDocValue(versionDocValue)

			local parentAssetAuxDigests = sourceDocValue.aux

			if not areAuxDigestsInParentAssetAux( versionAuxDigest, parentAssetAuxDigests ) then
				return Observable.throw( 'Version auxiliaries are not referenced in the parent asset document.' )
			end
		
			return docStore:setDocValue( versionDocValue )
		
				:map( function()
		
					return versionId
					
				end )
		
		end )
		
		:call( WFRxRunUntilDone )

end

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

--[[!
	\brief Get the count of existing auto versions for an asset.
	
	\param catalog (WFCatalog)
	\param assetId (string|number) The local or full doc ID of the asset.
	
	\return <code>Observable</code> The number of auto versions for the asset.
--]]

function WFCatVersions.getAutoVersionCountForAsset( catalog, assetId )

	WFArguments_check( "WFCatalog", "+string|number" )
	
	return catalog:indexStore():query(
		{
			sql = [[ SELECT COUNT(1)
					FROM versions
					WHERE assetId = ?
						AND createdManually = 0
						AND subtype = ? ]],
			assetId, 
			kVersionSubtypeEdit
		} )
		:take( 1 )
		:map( function( result )
			return result and result[ 1 ] and result[ 1 ][ 1 ]
		end )

end

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

--[[!
	\brief Checks if any manual version exist for passed assetIds.
	
	\param catalog (WFCatalog)
	\param fullDocIdsOfAssets (table) The full doc IDs of the assets to check.
	
	\return <code>Observable</code> Returns true if even one manual version exists, else false.
--]]

function WFCatVersions.assetsHaveManualVersions( wfCatalog, fullDocIdsOfAssets )

    WFArguments_check( "WFCatalog", "table" )

	if #fullDocIdsOfAssets == 0 then
        return Observable.just( false )
    end

	local assetIdsMap = KSTableUtils.invert( fullDocIdsOfAssets )
	
    return wfCatalog:indexStore():queryOnce( {
	
            sql = [[SELECT assetId 
                   FROM versions 
                   WHERE createdManually = 1 
                   AND subtype = ?]],
            kVersionSubtypeEdit,
			singleColumnQuery = true,
			
        } )
    :map( function( versionAssetIds )   

		for _, assetId in ipairs( versionAssetIds ) do
			if assetIdsMap[ assetId ] then
				return true
			end
		end		
		
        return false
		
    end )
	
end

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

local function formatVersionRow( catalog, row, assetDocValue, optImageType )

	-- SPAM [jandrew 2021-05-24]: LRMA-4203003: this prevents the get*VersionsByAssetId APIs
	-- from returning incomplete version documents caused by permission issues. In most cases,
	-- clients will not call these APIs unless the user's current role is an editor, or
	-- if they're the owner of the asset. However, if the associated models are left open
	-- during a role downgrade from editor -> contributor/viewer, the API can fire with
	-- versions that are technically incomplete. To detect this, we look for the creation
	-- date field. This field is always included in complete version documents, but is omitted
	-- from version documents sent to viewers and contributors. We look at this specific field
	-- instead of role field in the spaceUser document because, after a role downgrade, we may
	-- receive the updated version documents BEFORE we receive the role update in the spaceUser
	-- document.

	local creationDate = row[ 2 ]

	if creationDate then

		local externalXmpRelativePath = row[ 9 ]

		local catalogId = catalog:ozCatalogId()
		local externalizedDevelopSettingsPath
				= WFCatAssetUtils.absolutePathToDevelopSettings( catalogId, externalXmpRelativePath )

		local versionDoc = row.docValue

		local versionComplete, versionIsLocal = WFCatAssetAux.isVersionComplete( assetDocValue, versionDoc, optImageType )

		local hasDevelopFilters = KSTableUtils_derefOpt(row, 'docValue', 'develop', 'filters') ~= nil

		if versionComplete then

			local version = {
				id = row[ 1 ],
				date = creationDate,
				name = row[ 3 ],
				type = row[ 4 ],
				author = row[ 5 ],
				authorFullname = row[ 6 ],
				authorFormFactor = row[ 7 ],
				embeddedDevelopSettings = row[ 8 ],
				hasExternalizedDevelopSettings = intToBool( row[ 14 ] ),
				externalizedDevelopSettingsPath = externalizedDevelopSettingsPath,
				croppedWidth = row[ 10 ],
				croppedHeight = row[ 11 ],
				userOrientation = row[ 12 ],
				createdManually = intToBool( row[ 13 ] ),
				linkedLocationId = row[ 15 ],
				linkedLocationSource = row[ 16 ],
				linkedLocationSha = row[ 17 ],
				versionIsLocal = versionIsLocal,
				hasDevelopFilters = hasDevelopFilters
			}

			local versionMasks = KSTableUtils_derefOpt( versionDoc, 'develop', 'masks' )
			local versionFilters = KSTableUtils_derefOpt( versionDoc, 'develop', 'filters' )

			if versionMasks then
				KSTableUtils_setNested( version, versionMasks, 'developMasks' )
			end

			if versionFilters then
				KSTableUtils_setNested( version, versionFilters, 'developFilters' )
			end

			return version

		end

	end

end

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

local function getVersionsStreamForAsset( catalog, assetId, includeFakeImportStateVersion, manualVersionsOnly, autoVersionsOnly, optImageType )

	WFArguments_check( "WFCatalog", "+string|number", '?boolean', '?boolean', '?boolean', '?string' )

	-- The type and importedByUser fields are included to allow clients
	-- to filter this data.

	local fullDocIdStream = Observable.just( assetId )

	if type( assetId ) == 'number' then
		fullDocIdStream = catalog:docStore():localIdToDocId( assetId )
	end

	local versionsQuery = function( docId )

		local manualStatment = ""

		if manualVersionsOnly then
			manualStatment = "AND createdManually = 1"
		elseif autoVersionsOnly then
			manualStatment = "AND createdManually = 0"
		end

		return catalog:indexStore():query(
			{

				sql = string.format(
						[[ SELECT versions.docId, creationDate, name, subtype,
								importedByUser, feedback_user.fullname, importedByFormFactor,
								embeddedDevelopSettings, externalXmpRelativePath,
								croppedWidth, croppedHeight, userOrientation,
								createdManually, hasExternalizedDevelopSettings,
								linkedLocationId, linkedLocationSource, linkedLocationSha
							FROM versions
								LEFT OUTER JOIN feedback_user ON versions.importedByUser == feedback_user.userId
							WHERE assetId = ?
								AND subtype = ?
								AND incomplete = 0
								%s
							ORDER BY creationDate ASC ]],
						manualStatment ),
				calm = 1,
				docId,
				kVersionSubtypeEdit
			} )

	end

	return fullDocIdStream

		:flatMap( function( docId )

			return Observable.combineLatest(
					versionsQuery( docId ),
					catalog:docStore():docValue( docId ) )

		end )

		:flatMap( function( versionRows, assetDocValue )

			local versionDocIds = KSTableUtils.extractColumnAsTable( versionRows, 1 )

			local versionDocIdsAsKeys = KSTableUtils.invert( versionDocIds )

			return catalog:docStore():bulkDocValue( versionDocIds )

				:map( function( versionDocValues )

					for i, versionDocValue in ipairs( versionDocValues ) do

						local versionId = versionDocValue._id

						local versionRowsIndex = versionDocIdsAsKeys[ versionId ]

						versionRows[ versionRowsIndex ].docValue = versionDocValue

					end

					return versionRows, assetDocValue

				end )

		end )

		:map( function( versionRows, assetDocValue )

			local versions = {}

			if includeFakeImportStateVersion then

				-- Create version Zero. This represents the asset in it's original, imported state.

				local originalImportDate = KSTableUtils_derefOpt( assetDocValue, 'importSource', 'importTimestamp' )

				local originalOrientation, originalWidth, originalHeight
						= WFCatIndexUtils_getOrientationWidthAndHeight( assetDocValue )

				local originalImportedByUser
						= KSTableUtils_derefOpt( assetDocValue, '_serverProvidedMetadata', 'importedByUser', 'id' )

				local xmpDevelopLocal = not not WFCatAssetUtils.getLocalExternalDevelopSettingsPath( assetDocValue )
				local needsAuxDownload = WFCatAssetAux.needsAuxDownloadForDevelop( assetDocValue, optImageType )

				local versionZero = {
					date = originalImportDate,
					type = 'edit',
					author = originalImportedByUser,
					createdManually = false,
					croppedWidth = originalWidth,
					croppedHeight = originalHeight,
					userOrientation = originalOrientation,
					isFakeImportStateVersion = true,
					versionIsLocal = xmpDevelopLocal and not needsAuxDownload
				}

				versions[ #versions + 1 ] = versionZero

			end

			for _, row in ipairs( versionRows ) do

				local maybeVersion = formatVersionRow( catalog, row, assetDocValue, optImageType )

				if maybeVersion then
					versions[ #versions + 1 ] = maybeVersion
				end

			end

			return versions

		end )

end

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

--[[!
	\brief Fetch All Versions of a particular asset, sorted chronologically.
		(Newest first) 
	
	\param catalog (WFCatalog)
	\param assetId (string|number) The local or full doc ID of an asset.
	\param includeFakeImportStateVersion (optional boolean) If true, a fake version
		representing the original/reset state of the photo upon import is included
		in the response.
		
	\return <code>Observable</code> An observable stream that fires with a table
		containing all versions of the specified asset. Example:
		
		{
			{
				id = <version ID>,
				date = <creation date>,
				name = <version name>,
				type = <"version" or eventually "remix">
				author = <author full name>
				authorFormFactor = <"Tablet" or "Phone" or "Computer" or ...>
				embeddedDevelopSettings = < xmp settings string >
					-- This is only populated for assets with internalized develop settings.
				externalizedDevelopSettingsPath = <path to the externalized develop settings>
					-- If the settings are externalized but have not been downloaded,
					-- this path will be nil.
				croppedWidth = < number >
				croppedHeight = < number >
				userOrientation = < number >
				createdManually = < boolean >
			},
			...
		}
		
--]]

function WFCatVersions.getAllVersionsStreamForAsset( catalog, assetId, includeFakeImportStateVersion, optImageType )

	logger:infof( "Fetching all versions for asset %s", assetId )
	
	local manualVersionsOnly = false
	local autoVersionsOnly = false

	return getVersionsStreamForAsset( catalog, assetId, includeFakeImportStateVersion, manualVersionsOnly, autoVersionsOnly, optImageType )

end

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

--[[!
	\brief Fetch manual Versions of a particular asset, sorted chronologically.
		(Newest first) 
	
	\param catalog (WFCatalog)
	\param assetId (string|number) The local or full doc ID of an asset.
	\param includeFakeImportStateVersion (optional boolean) If true, a fake version
		representing the original/reset state of the photo upon import is included
		in the response.
		
	\return <code>Observable</code> An observable stream that fires with a table
		containing all versions of the specified asset. 
		Example please see WFCatVersions.getAllVersionsStreamForAsset
		
--]]

function WFCatVersions.getManualVersionsStreamForAsset( catalog, assetId, includeFakeImportStateVersion, optImageType )

	logger:infof( "Fetching manual versions for asset %s", assetId )

	return getVersionsStreamForAsset( catalog, assetId, includeFakeImportStateVersion, true, false, optImageType )

end

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

--[[!
	\brief Fetch auto Versions of a particular asset, sorted chronologically.
		(Newest first) 
	
	\param catalog (WFCatalog)
	\param assetId (string|number) The local or full doc ID of an asset.
	\param includeFakeImportStateVersion (optional boolean) If true, a fake version
		representing the original/reset state of the photo upon import is included
		in the response.
	\param optImageType ( optional string ) Variable that indicates what size of aux is being requested. Available types:
		'original' - Fetch full-size if possible, otherwise proxy size
		'proxy-2560' - Fetch proxy size. If not available WF will make a resample request to Oz.
		
	\return <code>Observable</code> An observable stream that fires with a table
		containing all versions of the specified asset. 
		Example please see WFCatVersions.getAllVersionsStreamForAsset
		
--]]

function WFCatVersions.getAutoVersionsStreamForAsset( catalog, assetId, includeFakeImportStateVersion, optImageType )

	logger:infof( "Fetching auto versions for asset %s", assetId )

	return getVersionsStreamForAsset( catalog, assetId, includeFakeImportStateVersion, false, true, optImageType )

end

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

--[[!
	\brief Fetch information about a particular Version

	\param catalog (WFCatalog)
	\param versionId (string) The full doc ID of the Version.

	\return <code>Observable</code> An observable stream that fires with a table
		containing information about the Version.

--]]

function WFCatVersions.getVersionInfo( catalog, versionId, optImageType )

	WFArguments_check( "WFCatalog", "+string", '?string' )

	local kNoAssetErrorString = "The version does not have an associated Asset. This should be impossible."

	return catalog:indexStore():queryOnce(
		{

			sql = [[ SELECT assetId, creationDate, name, subtype,
							importedByUser, feedback_user.fullname, importedByFormFactor,
							embeddedDevelopSettings, externalXmpRelativePath,
							croppedWidth, croppedHeight, userOrientation,
							createdManually, hasExternalizedDevelopSettings,
							linkedLocationId, linkedLocationSource, linkedLocationSha
						FROM versions
							LEFT OUTER JOIN feedback_user ON versions.importedByUser == feedback_user.userId
						WHERE versions.docId = ? ]],
			versionId

		} )

		:flatMap( function( versionRows )

			local versionRow = versionRows[ 1 ] or {}

			local assetId = versionRow[ 1 ]

			if not assetId then
				error( kNoAssetErrorString )
			end

			-- FIXME [jandrew 2023-04-27]: At some point we should clean this up and
			-- clean up getVersionsStreamForAsset, so that we don't need to both
			-- query the Versions table *and* fetch the version document from the doc store.

			return Observable.combineLatest(
					Observable.just( versionRow ),
					catalog:docStore():docValue( versionId ),
					catalog:docStore():docValue( assetId ) )

		end )

		:map( function( versionRow, versionDocValue, assetDocValue )

			if versionDocValue and assetDocValue then

				versionRow[ 1 ] = versionId
					-- Format versionRow the same way as getVersionsStreamForAsset so that
					-- we can use the formatVersionRow helper function.

				versionRow.docValue = versionDocValue
				return formatVersionRow( catalog, versionRow, assetDocValue, optImageType )

			else

				error( kNoAssetErrorString )

			end

		end )

end

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

--[[!
	\brief Rename a Version
	
	\param catalog (WFCatalog)
	\param versionId (string|number) The local or full doc ID of a Version.
	\param name (string) The new name of the Version.
	
	\return <code>Observable</code> Resolves to true once the version
		has been renamed successfully, or throws on error when user 
		is not authorized to create version.
--]]

function WFCatVersions.renameVersion( catalog, versionId, versionName )

	WFArguments_check( "WFCatalog", "+string|number", "string" )

	return checkUserAuthorization( catalog, versionId )
	
		:flatMap( function()

			versionName = maybeMakeVersionNameValid( versionName )
			
			local docStore = catalog:docStore()
		
			local baseOverlay = WFCatUtils.createBaseOverlayWithUserUpdatedField( versionId )
				
			baseOverlay.name = versionName
			baseOverlay.createdManually = true
				-- If an automatic version is renamed later by the end user, this should set
				-- to true to indicate that it's of interest to the end user.
				
			return docStore:applyOverlay( baseOverlay )
				
				:map( returnTrue )
		
		end )

end

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

--[[!
	\brief Delete a Version
	
	\param catalog (WFCatalog)
	\param versionId (string|number) The local or full doc ID of a Version.
		
	\return <code>Observable</code> Resolvese to true once the Version
		has been deleted successfully, or throws on error when user 
		is not authorized to create version.
--]]

function WFCatVersions.deleteVersion( catalog, versionId )

	WFArguments_check( "WFCatalog", "+string|number" )

	return checkUserAuthorization( catalog, versionId )
	
		:flatMap( function()
			
			return catalog:docStore():deleteDoc( versionId )
				:map( returnTrue )
		
		end )
	
end

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

local function deleteVersionsForAsset( catalog, assetId, manualVersionsOnly, autoVersionsOnly, optNoLinkedLocation )

	WFArguments_check( "WFCatalog", "+string|number", "?boolean", "?boolean", '?boolean' )
	
	return checkUserAuthorization( catalog, assetId )
	
		:flatMap( function()
	
			local fullDocIdStream = Observable.just( assetId )
			
			if type( assetId ) == 'number' then
				fullDocIdStream = catalog:docStore():localIdToDocId( assetId )
			end
			
			return fullDocIdStream
			
				:flatMap( function( docId )
				
					local manualStatment = ""
				
					if manualVersionsOnly then
						manualStatment = "AND createdManually = 1"
					elseif autoVersionsOnly then
						manualStatment = "AND createdManually = 0"
					end

					if optNoLinkedLocation then
						manualStatment = manualStatment .. ' AND linkedLocationId IS NULL AND linkedLocationSource IS NULL'
					end
					
					local sql = string.format( 
									[[ SELECT docId, externalXmpRelativePath
										FROM versions
										WHERE assetId == ? 
											AND subtype == ? 
											%s ]],
								manualStatment )
				
					return catalog:indexStore():queryOnce(
						{
							sql = sql,
							quiesceUntilUpToDate = true,
							docId, 
							kVersionSubtypeEdit
						} )
				
				end )
				
				:flatMap( function( versionRows )
				
					return WFCatDelete.deleteDocsFromQueryResult( catalog:docStore(), versionRows )
					
				end )
				
				:map( returnTrue )
				
		end )

end

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

--[[!
	\brief Delete all Versions of a particular Asset.
	
	\param catalog (WFCatalog)
	\param assetId (string|number) The local or full doc ID of an asset.
		
	\return <code>Observable</code> Resolves to true once all Versions of the
		asset have been deleted successfully, or throws on error when user 
		is not authorized to create version.
--]]

function WFCatVersions.deleteAllVersionsForAsset( catalog, assetId )

	WFArguments_check( "WFCatalog", "+string|number" )
	
	return deleteVersionsForAsset( catalog, assetId )

end

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

--[[!
	\brief Delete all manual Versions of a particular Asset.
	
	\param catalog (WFCatalog)
	\param assetId (string|number) The local or full doc ID of an asset.
		
	\return <code>Observable</code> Resolves to true once all manual Versions of the
		asset have been deleted successfully, or throws on error when user 
		is not authorized to create version.
--]]

function WFCatVersions.deleteManualVersionsForAsset( catalog, assetId )

	WFArguments_check( "WFCatalog", "+string|number" )
	
	return deleteVersionsForAsset( catalog, assetId, true, false )

end

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

--[[!
	\brief Delete all auto Versions of a particular Asset.
	
	\param catalog (WFCatalog)
	\param assetId (string|number) The local or full doc ID of an asset.
	\param optNoLinkedLocation (bool) If true, only delete auto versions without a linkedLocationId set
		
	\return <code>Observable</code> Resolves to true once all auto Versions of the
		asset have been deleted successfully, or throws on error when user 
		is not authorized to create version.
--]]

function WFCatVersions.deleteAutoVersionsForAsset( catalog, assetId, optNoLinkedLocation )

	WFArguments_check( "WFCatalog", "+string|number" )
	
	return deleteVersionsForAsset( catalog, assetId, false, true, optNoLinkedLocation )

end

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

--[[!
	\brief Compare the provided settings against the settings within an asset
		or version document.
	
	\param catalog (WFCatalog)
	\param docId (string|number) The local or full doc ID of an asset or version
		to compare against.
	\param developSettings (table) A table containing the following develop settings:
		xmpCameraRaw (string)
		croppedHeight (number)
		croppedWidth (number)
		userOrientation (number)
		lensHref (string)
		cameraHref (string)
	
	\return <code>Observable</code> Resolves to true if the settings match, else
		returns false.
--]]

function WFCatVersions.compareCurrentSettings( catalog, docId, developSettings )

	WFArguments_check( "WFCatalog", "+string|number", "table" )

	return catalog:docStore():docValue( docId )
	
		:pluck( 'develop' )
	
		:map( function( docDevelopSettings )
	
			local docXmpCameraRaw = KSTableUtils_derefOpt( docDevelopSettings, 'xmpCameraRaw' )
			
			-- TODO [jandrew 2020-01-16]:  How should xmpCameraRaw settings be passed in? Always as strings?
			-- What about externalized deveop settings?
			
			if type( docXmpCameraRaw ) == 'table' and type( developSettings.xmpCameraRaw ) == 'table' then
				
				-- If both settings are tables, deep compare.
				
				if not KSTableUtils.deepEquals( docXmpCameraRaw, developSettings.xmpCameraRaw ) then
				
					return false
					
				end
			
			elseif type( docXmpCameraRaw ) == 'table' or type( developSettings.xmpCameraRaw ) == 'table' then
			
				-- If only one setting is in the table format, they don't match.
			
				return false
			
			elseif docXmpCameraRaw ~= developSettings.xmpCameraRaw then
			
				return false
				
			end
			
			local docCroppedHeight = KSTableUtils_derefOpt( docDevelopSettings, 'croppedHeight' )
			
			if docCroppedHeight ~= developSettings.croppedHeight then
				return false
			end
			
			local docCroppedWidth = KSTableUtils_derefOpt( docDevelopSettings, 'croppedWidth' )
			
			if docCroppedWidth ~= developSettings.croppedWidth then
				return false
			end
			
			local docUserOrientation = KSTableUtils_derefOpt( docDevelopSettings, 'userOrientation' )
			
			if docUserOrientation ~= developSettings.userOrientation then
				return false
			end
			
			local docLensHref = KSTableUtils_derefOpt( docDevelopSettings, 'profiles', 'lens', 'href' )
			
			if docLensHref ~= developSettings.lensHref then
				return false
			end
			
			local docCameraHref = KSTableUtils_derefOpt( docDevelopSettings, 'profiles', 'camera', 'href' )
	
			if docCameraHref ~= developSettings.cameraHref then
				return false
			end
	
			return true
	
		end )
	
end

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

--[[!
	\brief Download externalized xmp for a version asset.
	
	\param catalog (WFCatalog)
	\param docId (string|number) The local or full doc ID of the version for
		which to download the externalized xmp.
	\param saveFunc (function) A function that takes three arguments: 'catalog',
		a KSData instance containing the externalized xmp, and the version's
		docValue. This function should return the path of the saved xmp, the
		file size of the saved file, and the sha256 or contentId of the 
		externalized xmp on success, or throw on failure.
	
	\return <code>Observable</code> Resolves to the path of the saved externalized
		xmp on success, or throws on error.
--]]

function WFCatVersions.downloadExternalizedXmpForVersion( catalog, docId, saveFunc )

	WFArguments_check( "WFCatalog", "+string|number", "function" )

	return catalog:docStore():docValue( docId )
	
		:flatMapLatest( function( docValue )

			local href = KSTableUtils_derefOpt( docValue, "_serverProvidedMetadata", "links", "/rels/xmp/develop", "href" )
			
			if not href then
			
				return Observable.throw( "No externalized xmp href" )
				
			else
			
				local url = string.format( "%s%s", catalog:ozUser():serverBaseUrl(), href )
				
				
				local wfCatSpec = catalog:ozCatalogSpec()
				
				return wfCatSpec:get( url )
				
					:map( function( ... )
						return docValue, ...
					end )
					
			end
			
		end )
		
		:flatMapLatest( function( docValue, response, headers )

			if type( headers ) == "table" and headers.status_code == "200" then
			
				return saveFunc( catalog, response, docValue )
				
					:flatMapLatest( function( externalizedXmpPath, fileSize, xmpSha256OrContentId )
					
						if type( externalizedXmpPath ) == "string" then
							
							local sha256FieldName = WFPlatformInfo.isiOS and 'contentId' or 'sha256'

							local localOnly = { 
								files = {
									xmp_develop = {
										path = externalizedXmpPath,
										fileSize = fileSize,
										[ sha256FieldName ] = xmpSha256OrContentId,
									}
								}
							}
							
							 return catalog:docStore():applyOverlay {
									_id = docId,
									_localOnly = localOnly
								}
								
								:map( function()
									return docValue, externalizedXmpPath
								end )
							
						else
							return Observable.throw( "saveFunc returned non-string path" )
						end
						
					end )
					
			elseif type( headers ) == "table" then
			
				return Observable.throw(
					string.format "version xmp download failed with status code %s",
					headers.status_code )

			else

				return Observable.throw(
					string.format( "unexpected response to externalized xmp download attempt for %s", docId ) )

			end
			
			-- Just in case....
			return Observable.throw(
				string.format(
					"externalized xmp download attempt resulted in a response of type %s and headers of type %s",
					type( response ), type( headers ) ) )
					
		end )

		:flatMap( function( docValue, externalizedXmpPath )

			return WFCatAssetAux.ensureAuxFilesExistLocally( catalog, docValue )

				:map( function()

					return externalizedXmpPath

				end )

		end )

end

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

--[[!
	\brief Download aux files for a version document.

	\param catalog (WFCatalog)
	\param docId (string|number) The local or full doc ID of the version for
		which to download the auxiliaries.

	\return <code>Observable< ></code> Fires with a table when done:
		{
			success = <boolean>,
			failedToDownloadDigests = <table of aux digests that cannot be downloaded>
		}
--]]

function WFCatVersions.downloadAuxFilesForVersion( catalog, docId, optImageType )

	WFArguments_check( "WFCatalog", "+string|number", "?string" )

	return catalog:docStore():docValue( docId )

		:flatMap( function( docValue )

			return WFCatAssetAux.ensureAuxFilesExistLocally( catalog, docValue, optImageType )

		end )

end

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

--[[!
	\brief Get analytics for the versions in a user catalog.
	
	\param catalog (WFCatalog)
		
	\return <code>Observable</code> Resolves to a table of
		Versions metrics. Metrics include a count of implicitly create
		versions, manually created versions, and versions which sourced from an implicit import
--]]

function WFCatVersions.getVersionsAnalytics( catalog )

	WFArguments_check( "WFCatalog" )
	
	return catalog:indexStore():queryOnce(
		{
			sql = [[ SELECT createdManually, linkedLocationId
						FROM versions
						WHERE subtype = ? ]],
			quiesceUntilUpToDate = true,
			kVersionSubtypeEdit
		} )
	
		:map( function( rows )
		
			local implicitVersionsCount = 0
			local manualVersionsCount = 0
			local implicitImportVersionCount = 0
			
			for _, row in ipairs( rows ) do

				local createdManually = row[ 1 ]
				local linkedLocationId = row[ 2 ]
			
				if createdManually == 1 then

					manualVersionsCount = manualVersionsCount + 1

				else

					implicitVersionsCount = implicitVersionsCount + 1

				end

				if linkedLocationId then

					implicitImportVersionCount = implicitImportVersionCount + 1

				end
			
			end
			
			return {
				implicitVersionsCount = implicitVersionsCount,
				manualVersionsCount = manualVersionsCount,
				implicitImportVersionCount = implicitImportVersionCount
			}
			
		end )

end

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

--[[!
	\brief Deletes the previous auto-version for a given asset,
		if it belongs to the current user and it falls within
		a specified time window. If the latest auto-version
		is from another user, then the previous auto-version
		of the current user wont be deleted to preserve history.
		
	\param catalog (WFCatalog)
	\param assetId (string|number) The local or full doc ID of an asset.
	\param withinSeconds (number) The time window in seconds.
	
	\return <code>Observable</code> Resolves to a table of Versions metrics.
--]]

function WFCatVersions.maybeDeleteTempAutoVersion( catalog, assetId, withinSeconds )

	WFArguments_check( "WFCatalog" )
	
	return catalog:indexStore():queryOnce(
		{
			sql = [[ SELECT docId, creationDate, importedByUser
						FROM versions 
						WHERE assetId == ? 
							AND createdManually = 0
							AND subtype == ?
						ORDER BY creationDate DESC
						LIMIT 1]],
			quiesceUntilUpToDate = true,
			assetId, 
			kVersionSubtypeEdit
		} )
	
		:flatMap( function( rows )
		
			-- Delete the auto-version only if the latest auto-version
			-- is from the current user( importedByUser is NULL )
			-- and the creationDate is within the given time
			
			if rows and rows[ 1 ] then
				
				local docId = rows [ 1 ][ 1 ]
				local creationDate = rows [ 1 ][ 2 ] 
				local importedByUser = rows [ 1 ][ 3 ]
				
				local now = KSDate.timeInIso8601Format()
				local timeDiffInSecs = KSDate.differenceInSeconds( creationDate, now )
				
				if timeDiffInSecs < withinSeconds and not importedByUser then
				
					return WFCatDelete.deleteDocs( catalog:docStore(), { docId } )
						:map( function()
							return docId
						end )
					
				end
			end
			
			return Observable.just( nil )
			
		end )

end

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

return WFCatVersions
