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

WFCatPixelPathGenerator.lua

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

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.os
local KSDate = require 'KSDate'
local KSPathUtils = require 'KSPathUtils'
local KSTableUtils = require 'KSTableUtils'
local WFFileUtils = require 'WFFileUtils'

	-- WF.core.utils
local WFArguments = require 'WFArguments'
local _ = require 'WFArguments_uuid'


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

local check = assert( WFArguments.check )
KSDate.registerArgumentChecker()

local child = assert( KSPathUtils.child )
local parent = assert( KSPathUtils.parent )

local str_byte = assert( string.byte )
local math_mod = assert( math.mod )
local math_floor = assert( math.floor )

--------------------------------------------------------------------------------
-- Given a 5-bit value, what is the base32 character for it?

local base32Values = {
	'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h',
	'j', 'k', 'm', 'n', 'p', 'q', 'r', 's',
	't', 'u', 'v', 'w', 'x', 'y', 'z', '1',
	'2', '3', '4', '5', '6', '7', '8', '9',
}

local base32Zero = base32Values[ 1 ]

--------------------------------------------------------------------------------
-- Given a byte for a character, what is its hex value?

local hexLookup = {
	     0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
	 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
	 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
	 0,  1,  2,  3,  4,  5,  6,  7,  8,  9,  0,  0,  0,  0,  0,  0,
	 0, 10, 11, 12, 13, 14, 15,  0,  0,  0,  0,  0,  0,  0,  0,  0,
	 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
	 0, 10, 11, 12, 13, 14, 15,  0,  0,  0,  0,  0,  0,  0,  0,  0,
	 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
}

--------------------------------------------------------------------------------
-- Precomputed useful powers of two:

local powerOf2 = {
	2, 4, 8, 16, 32,
}

--------------------------------------------------------------------------------
-- Given a 4-bit value, what is the hex character for it?

local hexValues = {
	"0", "1", "2", "3", "4", "5", "6", "7",
	"8", "9", "a", "b", "c", "d", "e", "f"
}

--------------------------------------------------------------------------------
-- Given the byte for a character, what is its base32 value?

local base32Lookup = {
	     0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
	 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
	 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
	 0, 23, 24, 25, 26, 27, 28, 29, 30, 31,  0,  0,  0,  0,  0,  0,
	 0,  0,  1,  2,  3,  4,  5,  6,  7,  0,  8,  9,  0, 10, 11,  0,
	12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,  0,  0,  0,  0,  0,
	 0,  0,  1,  2,  3,  4,  5,  6,  7,  0,  8,  9,  0, 10, 11,  0,
	12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,  0,  0,  0,  0,  0,
}

--------------------------------------------------------------------------------
--[[
	base32 is our own encoding standard similar to base64, but that does not
	use both uppercase and lowercase letters. Unlike hexadecimal, which encodes
	4 bits per character, base32 encodes 5 bits per character.

	RATIONALE: When mapping the approximately 175 bits of data we need to
	encode into filenames, this encoding allows us to represent that data in
	55 characters instead of 67. On FAT32 volumes, being able to use filenames
	less than 59 characters gives us the ability to store an additional 2185
	files per directory.

	base32 uses these characters, in this order to encode the 32 values represented
	by 5 bits of data:

		abcdefghjkmnpqrstuvwxyz123456789

	NOTE: We omit '0', 'i', 'l', and 'o', which could be mistaken for one
	another.

	Some source sequences will not map directly to a multiple of 5 bits. In such
	cases the source is padded out with imaginary zero bits at the start of the
	sequences. For example a hex input string of 'fc' corresponds to the binary
	sequence '11111100'. When mapped to base32, this is padded to ten bits:
	'0011111100', and then encoded as two characters 'h' and '6'.
]]--

local function convertHexStringToBase32( hexStr,  optResultLen )

	check( 'string', '?number' )

	assert( not hexStr:match( '[^0-9a-fA-F]' ), 'Not a hex string' )

	local residualBits = math_mod( #hexStr, 5 )
	local value = 0
	local result = {}

	for charIndex = 1, #hexStr do

		local b = str_byte( hexStr, charIndex )

		-- We know for certain this is a hex string so we can
		-- optimize converting it to a value.

		value = value * 16 + hexLookup[ b ]
		residualBits = residualBits + 4

		local p, base32LookupOffset

		while residualBits >= 5 do

			residualBits = residualBits - 5
			p = powerOf2[ residualBits ] or 1
			base32LookupOffset = math_floor( value / p )
			value = value - base32LookupOffset * p
			result[ #result + 1 ] = base32Values[ base32LookupOffset + 1 ]

		end

	end

	assert( residualBits == 0, 'Program Error: residualBits should always be zero' )
		-- It's always zero because we started with the correct value for
		-- residualBits above.

	if optResultLen then

		while #result > optResultLen do

			if result[ 1 ] ~= base32Zero then
				error( string.format( 'When specifying a result length, removed digits must be base32Zero(\'%s\'). The result was %s.',
									  base32Zero, table.concat( result ) ) )
			end

			table.remove( result, 1 )

		end

	end

	return table.concat( result )

end

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

local function convertBase32ToHexString( base32, optResultLen )

	check( 'string', '?number' )

	assert( not base32:match( '[^abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ123456789]' ),
			'Not a base32 string: ' .. tostring(base32) )

	local residualBits = 3 - math_mod( #base32 + 3, 4 )
	local value = 0
	local result = {}

	for charIndex = 1, #base32 do

		local b = str_byte( base32, charIndex )

		-- We know for certain this is a base32 string so we can
		-- optimize converting it to a value.

		value = value * 32 + base32Lookup[ b ]
		residualBits = residualBits + 5

		local p, hexValueOffset

		while residualBits >= 4 do

			residualBits = residualBits - 4
			p = powerOf2[ residualBits ] or 1
			hexValueOffset = math_floor( value / p )
			value = value - hexValueOffset * p
			result[ #result + 1 ] = hexValues[ hexValueOffset + 1 ]

		end

	end

	assert( residualBits == 0, 'Program Error: residualBits should always be zero' )
		-- It's always zero because we started with the correct value for
		-- residualBits above.

	if optResultLen then

		while #result > optResultLen do

			if result[ 1 ] ~= '0' then
				error( string.format( 'When specifying a result length, removed digits must be zeros, result was \'%s\'',
									  table.concat( result ) ) )

			end

			table.remove( result, 1 )

		end

	end

	return table.concat( result )

end

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


local function curry( f )

	local retVals = {}

	return function( ... )

		local nArgs = select( '#', ... )

		local retVal = retVals

		for i = 1, nArgs do

			local a = select( i, ... )

			if i < nArgs then

				retVal = retVal[ a ] or {}

			else

				retVal = retVal[ a ] or f( ... )

			end

		end

		return retVal

	end

end

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

-- @mark WFCatPixelPathGenerator namespace

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

local function pathFragmentForUuid( uuid )

	local first12bits = uuid:sub( 1, 3 )
	local remainder = uuid:sub( 4 )

	return child( first12bits, remainder )

end

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

local _longEdgeDivisor = 2

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

local function validateLongEdge( longEdge )

	if longEdge == 'full' or longEdge == "1080p" or longEdge == "1080p709" or longEdge == "720p" or  longEdge == "360p" then
		return longEdge
	end

	assert( type( longEdge ) == 'number',
			"longEdge must be the string \"full\" or numeric" )

	assert( longEdge > 1, "Long edge must be > 1" )

	assert( longEdge < 65535, "Long edge must be < 65535" )

	if longEdge / _longEdgeDivisor ~= math.floor( longEdge / _longEdgeDivisor ) then

		error( string.format( "Long edge must be multiple of %d",
							  _longEdgeDivisor ) )

	end

	return longEdge

end

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

-- offest so we don't collide with image rendition sizes
local _videoM4PRenditionSizeOffset = 2^14 -- keep within bounds for base32 encoding

local _stringToLongEdgeMap = {
	-- Must be 1:1 relationship
		[ "full" ] = 0,
		[ "1080p709" ] = 1081 + _videoM4PRenditionSizeOffset,
		[ "1080p" ] = 1080 + _videoM4PRenditionSizeOffset,
		[ "720p" ] = 720 + _videoM4PRenditionSizeOffset,
		[ "360p" ] = 360 + _videoM4PRenditionSizeOffset
	}

local _longEdgeToStringMap = KSTableUtils.invert( _stringToLongEdgeMap )

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

local function convertStringLongEdgeToNumber( longEdgeStr )
	
	local longEdgeNumber = _stringToLongEdgeMap[ longEdgeStr ]
	
	if not longEdgeNumber then
		error( string.format( "Can't convert long edge %s to number", longEdgeStr ) )
	end
	
	return longEdgeNumber
	
end

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

local function convertNumberToLongEdgeString( longEdgeNum )


	return _longEdgeToStringMap[ longEdgeNum ]
	
end

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

local function encodeLongEdgeNumerically( longEdge )

	if type( longEdge ) == "string" then
		return convertStringLongEdgeToNumber( longEdge )
	end

	return longEdge / _longEdgeDivisor

end

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

local function numericLongEdgeEncodingToLongEdge( numericLongEdgeEncoding )
	
	local maybeLongEdgeString = convertNumberToLongEdgeString( numericLongEdgeEncoding )
	
	if type( maybeLongEdgeString ) == "string" then
		return maybeLongEdgeString
	end
	
	return _longEdgeDivisor * numericLongEdgeEncoding

end

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

local function longEdgeAsBase32( longEdge )

	local numericLongEdgeEncoding =
			encodeLongEdgeNumerically( validateLongEdge( longEdge ) )

	return convertHexStringToBase32( string.format( '%03x',
									 numericLongEdgeEncoding ), 3 )

end

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

local function base32ToLongEdge( base32LongEdge )

	return validateLongEdge(
			numericLongEdgeEncodingToLongEdge(
				tonumber( convertBase32ToHexString( base32LongEdge ), 16 ) ) )

end

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

local _assetIdRenditionFolderNames

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

local KSTableUtils_shallowcopy = KSTableUtils.shallowcopy

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

local function assetIdRenditionFolderNames()

	if not _assetIdRenditionFolderNames then

		_assetIdRenditionFolderNames = {}

		local bitsPerHex = 4

		local bitsPerBase32 = 5

		local randomUUID = require 'KSIdGenerator'.uuidInLowercaseHex() 

		local bitsPerUUID = bitsPerHex * string.len( randomUUID )

		local bitsInMostSignifcantBase32UUID = 
				math_mod( bitsPerUUID, bitsPerBase32 )

		for i = 1, 2 ^ bitsInMostSignifcantBase32UUID do

			for j = 1, 32 do

				_assetIdRenditionFolderNames[ #_assetIdRenditionFolderNames + 1 ] = 
					string.format( "%s%s", base32Values[ i], base32Values[ j ] )

			end

		end

	end

	return KSTableUtils_shallowcopy( _assetIdRenditionFolderNames )

end

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

local _validAssetIdRenditionFolderNames

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

local function isValidAssetIdRenditionSubfolderName( assetIdRenditionFolderName )

	_validAssetIdRenditionFolderNames = _validAssetIdRenditionFolderNames or
		KSTableUtils.invert( assetIdRenditionFolderNames() )

	return not not 
			_validAssetIdRenditionFolderNames[ assetIdRenditionFolderName ]

end

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

local function getPreviewsFolder( baseBinariesPath )

	return child( baseBinariesPath, 'previews' )

end

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

getPreviewsFolder = curry( getPreviewsFolder )

--------------------------------------------------------------------------------
--[[!
	\brief A subfolder of the previews directory

	\param assetIdRenditionFolderName (uuid) This must be a folder name that 
	could contain previews for some assetId. IOW, if it's impossible for the
	folder name to contain any assetId's preview files, it's invalid.

	\return (String, folder path) The full previews subfolder path, otherwise
	this function throws!
	
]]--

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

local function getPreviewsSubfolder( baseBinariesPath, 
										assetIdRenditionFolderName )

	if not isValidAssetIdRenditionSubfolderName( 
				assetIdRenditionFolderName ) then

		error( string.format( "'%s' is not a valid assetId rendtion folder name",
								assetIdRenditionFolderName ) )

	end

	return child( getPreviewsFolder( baseBinariesPath ), 
						assetIdRenditionFolderName )

end

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

local function splitUUID( uuid )

	check( 'uuid' )

	local base32UUID = convertHexStringToBase32( uuid ) 

	return string.sub( base32UUID, 1, 2 ), string.sub( base32UUID, 3 )

end

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

local function pathInfoForAssetId( baseBinariesPath, uuid )

	check( '+string', 'uuid' )

	local assetIdFolderName, assetIdRemainder = splitUUID( uuid )

	return getPreviewsSubfolder( baseBinariesPath, assetIdFolderName ),
			assetIdRemainder

end

--------------------------------------------------------------------------------
--[[!
	\brief Calculate the containing folder name for all of an assetId's preview
	files

	\param uuid (uuid) The asset id. It must be a 32-character lowercase
		hexadecimal string

	\return (String, folder name) The parent folder name of all of the preview
	files for the assetId.
	
]]--

local function renditionFolderNameForAssetId( uuid )

	check( 'uuid' )

	local assetIdFolderPath = splitUUID( uuid )

	return assetIdFolderPath

end

--------------------------------------------------------------------------------
--[[!
	\brief Calculate the containing folder for all of an assetId's preview files

	\param uuid (uuid) The asset id. It must be a 32-character lowercase
		hexadecimal string

	\return (String, local file path) A folder path where all of the preview
	files for the assetId may be found.  The folder path is not guaranteed to
	exist. The folder path is not guaranteed to contain preview files for the 
	assetId. Use WFCatPixelPathGenerator.assetIdDigestAndLongEdgeFromPreviewPath 
	for any paths inside this folder to check if the file is actually a preview
	for the assetId.
	
]]--

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

local function renditionFolderPathForAssetId( baseBinariesPath, uuid )

	check( '+string', 'uuid' )

	local assetIdFolderPath = pathInfoForAssetId( baseBinariesPath, uuid )

	return assetIdFolderPath

end

--------------------------------------------------------------------------------
--[[!
	\brief Calculate the file path for a preview.

	\param uuid (uuid) The asset id. It must be a 32-character lowercase
		hexadecimal string

	\param digest (md5 digest) The md5 digest as represented as a 32-character
		lowercase hexadecimal string

	\param longEdge (non-zero positive integer|"full") Must be an even value
		greater than 1 and less than 65535 or the string "full". The string full
		means the longEdge will be math.max( croppedWidth, croppedHeight ) for
		the dev settings matching the digest. If the image is not cropped, "full"
		means math.max( fullImageW, fullImageH )

	\param createDirectory (boolean) If true then the directory
		for this preview will be created. Otherwise the directory
		is not created (useful when the caller is only trying to find existing
		files and is not preparing to write one).

	\param fileExtension (string) The file extension to append to create
		the final path.

	\return (String, local file path) A file path where the preview may be found
		or should be stored. The directory for this preview is guaranteed to
		exist.
]]--

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

local function pathForPreviewDigestAndLongEdgeImp( baseBinariesPath, uuid, digest,
												longEdge, createDirectory,
												fileExtension )

	check( '+string', 'uuid', 'uuid', '_', 'boolean', 'string' )

	local assetIdFolder, assetIdRemainder = 
			pathInfoForAssetId( baseBinariesPath, uuid )

	if createDirectory and not WFFileUtils.isPathOnOfflineVolume( assetIdFolder ) then

		WFFileUtils.mkdir( assetIdFolder )

	end

	local filename = string.format( "%s%s%s%s", 
									assetIdRemainder,
									convertHexStringToBase32( digest ),
									longEdgeAsBase32( longEdge ),
									fileExtension )

	return child( assetIdFolder, filename )

end

--------------------------------------------------------------------------------
--[[!
	\brief Calculate the file extention-less path for a preview.

	\param uuid (uuid) The asset id. It must be a 32-character lowercase
		hexadecimal string

	\param digest (md5 digest) The md5 digest as represented as a 32-character
		lowercase hexadecimal string

	\param longEdge (non-zero positive integer|"full") Must be an even value
		greater than 1 and less than 65535 or the string "full". The string full
		means the longEdge will be math.max( croppedWidth, croppedHeight ) for
		the dev settings matching the digest. If the image is not cropped, "full"
		means math.max( fullImageW, fullImageH )

	\param optCreateDirectory (optional boolean) If true (or omitted) then the
		directory for this preview will be created. Otherwise the directory
		is not created (useful when the caller is only trying to find existing
		files and is not preparing to write one).

	\return (String, local file path) A file path where the preview may be found
		or should be stored. The directory for this preview is guaranteed to
		exist.
]]--

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

local function pathForPreviewDigestAndLongEdge( baseBinariesPath, uuid, digest,
												longEdge, optCreateDirectory )

	check( '+string', 'uuid', 'uuid', '_', '?boolean' )

	local createDirectory = optCreateDirectory ~= false

	return pathForPreviewDigestAndLongEdgeImp( 
                baseBinariesPath, uuid, digest,
                longEdge, createDirectory, '' )
				
end

--------------------------------------------------------------------------------
--[[!
	\brief Calculate the file path for a preview that includes a .jpg extension.

	\param uuid (uuid) The asset id. It must be a 32-character lowercase
		hexadecimal string

	\param digest (md5 digest) The md5 digest as represented as a 32-character
		lowercase hexadecimal string

	\param longEdge (non-zero positive integer|"full") Must be an even value
		greater than 1 and less than 65535 or the string "full". The string full
		means the longEdge will be math.max( croppedWidth, croppedHeight ) for
		the dev settings matching the digest. If the image is not cropped, "full"
		means math.max( fullImageW, fullImageH )

	\param optCreateDirectory (optional boolean) If true (or omitted) then the
		directory for this preview will be created. Otherwise the directory
		is not created (useful when the caller is only trying to find existing
		files and is not preparing to write one).

	\return (String, local file path) A file path where the preview may be found
		or should be stored. The directory for this preview is guaranteed to
		exist.
]]--

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

local function pathForPreviewDigestAndLongEdgeJpg( baseBinariesPath, uuid, digest,
												longEdge, optCreateDirectory )

	check( '+string', 'uuid', 'uuid', '_', '?boolean' )

	local createDirectory = optCreateDirectory ~= false

	return pathForPreviewDigestAndLongEdgeImp( 
                baseBinariesPath, uuid, digest,
                longEdge, createDirectory, '.jpg' )
				
end

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

--[[!
	\brief Calculate the file path for a preview that includes a .mp4 extension.

	\param uuid (uuid) The asset id. It must be a 32-character lowercase
		hexadecimal string

	\param digest (md5 digest) The md5 digest as represented as a 32-character
		lowercase hexadecimal string
		
	-- TODO [btran 2022-03-31]: Update with video sizes
	
	\param longEdge (non-zero positive integer|"full") Must be an even value
		greater than 1 and less than 65535 or the string "full". The string full
		means the longEdge will be math.max( croppedWidth, croppedHeight ) for
		the dev settings matching the digest. If the image is not cropped, "full"
		means math.max( fullImageW, fullImageH )

	\param optCreateDirectory (optional boolean) If true (or omitted) then the
		directory for this preview will be created. Otherwise the directory
		is not created (useful when the caller is only trying to find existing
		files and is not preparing to write one).

	\return (String, local file path) A file path where the preview may be found
		or should be stored. The directory for this preview is guaranteed to
		exist.
]]--

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

local function pathForPreviewDigestAndLongEdgeMP4( baseBinariesPath, uuid, digest,
												longEdge, optCreateDirectory )

	check( '+string', 'uuid', 'uuid', '_', '?boolean' )

	local createDirectory = optCreateDirectory ~= false

	return pathForPreviewDigestAndLongEdgeImp( 
                baseBinariesPath, uuid, digest,
                longEdge, createDirectory, '.mp4' )
				
end

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

--[[!
	\brief Extract preview info from a path to a preview file.

	\param previewPath (string) A local file path where the preview
		was found.

	\return (uuid) The asset id. It will be a 32-character lowercase
		hexadecimal string
	\return (md5 digest) The md5 digest as represented as a 32-character
		lowercase hexadecimal string
	\return (non-zero positive integer|"full") the long edge dimension associated with
		this preview or the string "full". "full" means the path represents a
		1:1 render at the current devSettings. In that case, longEdge =
		math.max( croppedWidth, croppedHeight ) for the dev settings in the digest.
		If the image is not cropped, "full" means
		math.max( fullImageW, fullImageH )
]]--

local function assetIdDigestAndLongEdgeFromPreviewPath( previewPath )

	check( '+string' )

	assert( #previewPath > 56, "Path is too short to be a preview path" )

	local shift = 0
	
	if '.jpg' ~= string.lower( previewPath:sub( -4 ) ) and '.mp4' ~= string.lower( previewPath:sub( -4 ) ) then
		shift = 4
	end

	local encLongEdge = previewPath:sub( -7 + shift, -5 + shift )
	local longEdge = base32ToLongEdge( encLongEdge )

	local encDigest = previewPath:sub( -33 + shift, -8 + shift )
	local digest = convertBase32ToHexString( encDigest, 32 )

	local encAssetId = previewPath:sub( -60 + shift, -59 + shift ) .. previewPath:sub( -57 + shift, -34 + shift )
	local assetId = convertBase32ToHexString( encAssetId, 32 )
	
	return assetId, digest, longEdge

end

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

local function pathWithUuidFragment( basePath, typeOf, uuidPathFragment, extn )

	local uuidPath = child( typeOf, uuidPathFragment )
	
	uuidPath = string.format( "%s%s", uuidPath, extn )

	local childPath = child( basePath, uuidPath )

	local containingFolder = parent( childPath )

	if not WFFileUtils.isPathOnOfflineVolume( containingFolder ) then
		WFFileUtils.mkdir( containingFolder )
	end

	return childPath

end

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

local function pathForTypeAndUuid( basePath, typeOf, uuid, extn )

	local uuidPathFragment = pathFragmentForUuid( uuid )
	
	return pathWithUuidFragment( basePath, typeOf, uuidPathFragment, extn )

end

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

local function pathForStyleTypeAndUuid( basePath, typeOf, uuid, extn )

	return pathWithUuidFragment( basePath, typeOf, uuid, extn )

end

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

local function pathForProxyTypeAndUuidImp( baseBinariesPath, proxyType, uuid, extn )

	check( '+string', '+string', 'uuid', 'string' )

	local proxiesPath = child( baseBinariesPath, 'proxies' )

	return pathForTypeAndUuid( proxiesPath, proxyType, uuid, extn )

end

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

local function pathForProxyTypeAndUuid( baseBinariesPath, proxyType, uuid )

	check( '+string', '+string', 'uuid' )

	return pathForProxyTypeAndUuidImp( baseBinariesPath, proxyType, uuid, '' )

end

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

local function isMP4ProxyType( proxyType )

	if proxyType:find( '1080p$' ) or
			proxyType:find( '1080p709$' ) or
			proxyType:find( '720p$' ) or
			proxyType:find( '360p$' ) then

		return true
		
	end

	return false

end

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

local function pathForProxyTypeAndUuidWithExtension( baseBinariesPath, proxyType, uuid )

	check( '+string', '+string', 'uuid' )

	local extn = isMP4ProxyType( proxyType ) and '.mp4' or '.dng'

	return pathForProxyTypeAndUuidImp( baseBinariesPath, proxyType, uuid, extn )

end

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

local function pathForOriginal( baseBinariesPath, fileName, captureDate, 
								alternatePathIfDriveOffline, skipCreateDir )

	check( '+string', '+string', 'KSDate', '?+string', '?boolean' )
	
	local basePath = baseBinariesPath
	
	local volumeIsOnline = false
		-- Used to bypass a file system check that may not be needed.
		
	if alternatePathIfDriveOffline then
	
		if WFFileUtils.isPathOnOfflineVolume( basePath ) then
			basePath = alternatePathIfDriveOffline
		else
			volumeIsOnline = true
		end
		
	end
	
	local originalsPath = child( basePath, 'originals' )
	
	captureDate = captureDate:sub( 1, 10 )
	
	local folder = child( originalsPath, captureDate:sub( 1, 4 ) ) -- yyyy
	
	folder = child( folder, captureDate ) -- yyyy-mm-dd
	
	volumeIsOnline = volumeIsOnline or not WFFileUtils.isPathOnOfflineVolume( folder )
	
	if not skipCreateDir and volumeIsOnline then
		
		WFFileUtils.mkdir( folder )
		
	end
	
	return child( folder, fileName )

end

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

local function settingsXmpFolderName()
	return 'settings'
end

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

local function folderForSettingsXmp( baseBinariesPath )
	
	check( '+string' )
	
	local externalSettingsPath = child( baseBinariesPath, settingsXmpFolderName() )
	
	if not WFFileUtils.isPathOnOfflineVolume( externalSettingsPath ) then
		WFFileUtils.mkdir( externalSettingsPath )
	end
	
	return externalSettingsPath
	
end

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

local function pathForSettingsXmp( baseBinariesPath, sha256 )
	
	check( '+string', '+string' )
	
	local externalSettingsPath = folderForSettingsXmp( baseBinariesPath )
	
	return child( externalSettingsPath, sha256 )

end

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

local function pathForXmpSidecar( baseBinariesPath, uuid )

	check( '+string', 'uuid' )

	return pathForTypeAndUuid( baseBinariesPath, 'xmp', uuid, '.xmp' )

end

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

local function pathForStyle( baseBinariesPath, uuid )

	check( '+string', 'uuid' )

	return pathForStyleTypeAndUuid( baseBinariesPath, 'cr_settings', uuid, '.xmp' )

end

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

local function pathForPerson( baseBinariesPath, uuid )

	check( '+string', 'uuid' )
	
	return pathWithUuidFragment( baseBinariesPath, 'people', uuid, '' )

end

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

local function maybeStylePath( baseBinariesPath, path )

	check( '+string', '+string' )

	-- Currently returning true for any path which 
	-- is inside the base style path, with extension xmp
	
	local styleBasePath = child( baseBinariesPath, 'cr_settings' )
	local pathParentFolder = KSPathUtils.parent( path )
		
	return styleBasePath == pathParentFolder
		and path:sub( -4 ) == ".xmp"

end

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

local function pathForDCP( baseBinariesPath, fileName )

	check( '+string', '+string', '?boolean' )

	local cameraSettingsPath = child( baseBinariesPath, 'cr_cameraprofiles' )
	
	return child( cameraSettingsPath, fileName )

end

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

local function pathForStandardProfile( baseBinariesPath, relativePath )

	check( '+string', '+string' )

	local cameraSettingsPath = child( baseBinariesPath, 'cr_cameraprofiles' )
	
	return child( cameraSettingsPath, relativePath )

end
--==============================================================================

return { 

	assetIdRenditionFolderNames = assetIdRenditionFolderNames,

	getPreviewsSubfolder = curry( getPreviewsSubfolder ),

	renditionFolderNameForAssetId = renditionFolderNameForAssetId,

	renditionFolderPathForAssetId = renditionFolderPathForAssetId,

	pathForPreviewDigestAndLongEdge = pathForPreviewDigestAndLongEdge,

	pathForPreviewDigestAndLongEdgeJpg = pathForPreviewDigestAndLongEdgeJpg,
	
	pathForPreviewDigestAndLongEdgeMP4 = pathForPreviewDigestAndLongEdgeMP4,

	assetIdDigestAndLongEdgeFromPreviewPath = assetIdDigestAndLongEdgeFromPreviewPath,

	pathForProxyTypeAndUuid = pathForProxyTypeAndUuid,

	pathForProxyTypeAndUuidWithExtension = pathForProxyTypeAndUuidWithExtension,

	pathForOriginal = pathForOriginal,

	pathForSettingsXmp = pathForSettingsXmp,
	
	folderForSettingsXmp = folderForSettingsXmp,
	
	settingsXmpFolderName = settingsXmpFolderName,
	
	pathForXmpSidecar = pathForXmpSidecar,
	
	pathForStyle = pathForStyle,
	
	pathForPerson = pathForPerson,
	
	pathForDCP = pathForDCP,
	
	pathForStandardProfile = pathForStandardProfile,

	maybeStylePath = maybeStylePath,

	convertHexStringToBase32 = convertHexStringToBase32,

	convertBase32ToHexString = convertBase32ToHexString,

}
