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

WFFileUtils.lua

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

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

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

local WFConfiguration = require "WFConfiguration"

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

	-- WF.core.os
local KSDate = require 'KSDate'
local KSPathUtils = require 'KSPathUtils'
local WFFileUtilsImp = require 'WFFileUtilsImp'
local WFPlatformInfo = require 'WFPlatformInfo'

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

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

local logger = WFLogger( "WFFileUtils" )

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

local check = assert( WFArguments.check )

local mapGuidToVolume = {}

local WFFileUtilsImp_getVolumeMountPointFromGuid

if WFFileUtilsImp.getVolumeMountPointFromGuid then

	WFFileUtilsImp_getVolumeMountPointFromGuid = function( guid )

		local mountPoint, isNetworkDrive = WFFileUtilsImp.getVolumeMountPointFromGuid( guid )

		-- NOTE: Fix for NTFSForMac. This is to balance out the change
		-- made to return the mountPoint for drives that were previously returning
		-- a device node (/dev/disk***) as the guid.  This guid was problematic as
		-- it could change when user reboots or repartitions a drive.
		-- See change in WFFileUtilsImpCocoa.mm regarding NTFSForMac.
		if not mountPoint and WFPlatformInfo.isMac then
			if WFFileUtilsImp.getVolumeGuidForMountPoint( guid ) == guid then
				mountPoint = guid
				isNetworkDrive = true
			end
		end

		return mountPoint, isNetworkDrive

	end

end

--============================================================================--
--[[!
	\class WFFileUtils

	\brief This namespace provides common file system operations.
]]--

-- @mark WFFileUtils namespace

local WFFileUtils = {}

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

-- @mark basic file operations

--------------------------------------------------------------------------------
--[[!
	\brief Reports whether a folder or file exists.

	\param path (string) The path to confirm.

	\return (string or boolean) "directory" or "file" if the path references an
		existing folder or file, otherwise (boolean) false.
]]--

function WFFileUtils.exists( path )

	check( '+string' )

	return WFFileUtilsImp.exists( path )

end

if WFPlatformInfo.isDesktop then
--------------------------------------------------------------------------------
--[[!
	\brief Reports whether a path is absolute path or not.

	\param path (string) The path to confirm.

	\return (boolean) true if the path is absolute or false otherwise
]]--

function WFFileUtils.isAbsolutePath( path )

	check( '+string' )
	return WFFileUtilsImp.isAbsolutePath( path )

end

end

--------------------------------------------------------------------------------
--[[!
	\brief Reports whether a folder is empty.

	\param path (string) The path to the folder.

	\return (boolean) True if the folder is empty, othewise false.

	NOTE: On Mac/iOS, hidden files are ignored. A folder will be reported as empty
	if it only contains hidden files such as .DS_STORE

]]--

function WFFileUtils.empty( path )

	check( '+string' )

	return WFFileUtilsImp.empty( path )

end

--------------------------------------------------------------------------------
--[[!
	\brief Copies a file.

	\param src (string) The absolute path of the source file.
	\param dest (string) The absolute path of the destination.

	If source and destination strings are identical, this function is a no-op.

	\warning (April 2014): Unlike the old KSFileUtils.copy, this function
		throws a Lua error if unable to copy.

	\return dest (string) returns the second argument; useful for chaining
]]--

function WFFileUtils.copy( src, dest )

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

	if src == dest then
		return src
	end

	WFFileUtilsImp.copy( src, dest )

	return dest

end

--------------------------------------------------------------------------------
--[[!
	\brief Moves a file.

	\param src (string) The absolute path of the source file.
	\param dest (string) The absolute path of the destination.

	If source and destination strings are identical, this function is a no-op.

	\warning (April 2014): Unlike the old KSFileUtils.move, this function
		throws a Lua error if unable to move.
]]--

function WFFileUtils.move( src, dest )

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

	if src == dest then
		return src
	end

	return WFFileUtilsImp.move( src, dest )

end

--------------------------------------------------------------------------------
--[[!
	\brief Immediately deletes a file, or a folder with all of its contents.

	\warning Be careful with this. In most cases calling \c moveToTrash would be better.

	\param path (string) The absolute path of a file or folder.
	\param optOnlyDeleteDirIfEmpty ( boolean ) If true, the folder will be
	only be deleted if it's empty.

	\warning (April 2014): Unlike the old KSFileUtils.delete, this function
		throws a Lua error if unable to delete.
]]--

function WFFileUtils.delete( path, optOnlyDeleteDirIfEmpty )

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

	if WFFileUtilsImp.exists( path ) == 'directory' and optOnlyDeleteDirIfEmpty then

		if WFFileUtilsImp.empty( path ) then
			return WFFileUtilsImp.delete( path )
		else
			return --noOp
		end

	end

	return WFFileUtilsImp.delete( path )

end

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

--[[!
	\brief Immediately deletes if and only if it is a file, otherwise throws lua error.

	\warning Be careful with this. In most cases calling \c moveToTrash would be better.

	\param path (string) The absolute path of a file

	\warning (April 2014): Unlike the old KSFileUtils.delete, this function
		throws a Lua error if it is unable to delete the file.
]]--

function WFFileUtils.deleteFile( filePath )

	check( '+string' )

	-- If it is a directory, throw lua error.
	if WFFileUtilsImp.exists( filePath ) == 'directory' then
		error( string.format( 'deleteFile: "%s" is a directory', filePath ) )
	end

	-- If file doesn't exist, this is a harmless no-op.
	if not WFFileUtilsImp.exists( filePath ) then
		return
	end

	return WFFileUtilsImp.delete( filePath )

end

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

--[[!
	\brief Immediately deletes if and only if it is a directory, otherwise throws lua error.

	\warning Be careful with this. In most cases calling \c moveToTrash would be better.

	\param dirPath (string) The absolute path of a directory
	\param optOnlyDeleteDirIfEmpty ( boolean ) If true, the folder will be
	only be deleted if it's empty.

	\warning (April 2014): Unlike the old KSFileUtils.delete, this function
		throws a Lua error if it unable to delete the file.
]]--

function WFFileUtils.deleteDirectory( dirPath, optOnlyDeleteDirIfEmpty )

	check( '+string' )

	local res = WFFileUtilsImp.exists( dirPath )

	if res == 'directory' and ( not optOnlyDeleteDirIfEmpty or WFFileUtilsImp.empty( dirPath )) then
		return WFFileUtilsImp.delete( dirPath )
	-- If it is a file, throw lua error.
	elseif res == 'file' then 
		error( string.format( 'deleteDirectory: "%s" is a file', dirPath ) )
	else -- luacheck: ignore -- empty if branch
		-- If directory doesn't exist, this is a harmless no-op.
	end

end

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

--[[!
	\brief Moves a file to the Trash or Recycle folder for its volume.

	\param filePath (string) The absolute path of the file.

	\warning In iOS, there is no such thing as a Trash folder, so this function
		performs an immediate deletion.
]]--

function WFFileUtils.moveToTrash( filePath )

	check( '+string' )

	-- On Win10, we can do a "soft-delete" which sends the file to Recycle Bin.

	if WFPlatformInfo.isWindows then
		return WFFileUtilsImp.delete( filePath, false )
			-- false indicates soft-delete
	end

	-- Some platforms don't have a trash API. If that's the case here,
	-- just do a delete.

	local moveItemToTrash = WFFileUtilsImp.moveItemToTrash
	if not moveItemToTrash then
		return WFFileUtils.delete( filePath )
	end

	-- If file doesn't exist, this is a harmless no-op.

	if not WFFileUtils.exists( filePath ) then
		return
	end

	moveItemToTrash( filePath )

end

--------------------------------------------------------------------------------
--[[!
	\brief Creates a new folder. This function will create any intermediate
		folders along the way that might be missing. (In other words, you do not
		have to call mkdir for any parent folders.)

	\param path (string) The absolute path of the new folder.

	\warning (April 2014): Unlike the old KSFileUtils.mkdir, this function
		throws a Lua error if unable to create the folder.
]]--

function WFFileUtils.mkdir( path )

	check( '+string' )

	return WFFileUtilsImp.mkdir( path )

end

--------------------------------------------------------------------------------
--[[!
	\brief Perform an operation on a temporary file and move the temporary into
		a location outside the application's sandbox if successful.

	This function is mostly meaningful on Windows RT. Otherwise, it simply calls
	the function with the desired final path, skipping the temporary file generation.

	\param path (string) The absolute path of the intended final file.
	\param func (function) The function to call. It takes a single argument, which
		is the temporary (sandbox-approved) path for the file.

	\return whatever results or error were returned by \c func
]]--

function WFFileUtils.doForAuthorizedPath( path, func )

	check( '+string|table', 'function' )

    if type( path ) == 'table' then

        local collisionBehavior = path.collisionBehavior
        path = KSPathUtils.child( path.folder, path.filename )

        if collisionBehavior == 'GenerateUniqueName' then
            path = WFFileUtils.ensureFileNameIsUnique( path )
        elseif collisionBehavior == 'FailIfExists' then
            assert( not WFFileUtils.exists( path ),
                    "File already exists and collisionBehavior is FailIfExists" )
        end

    end

    return func( path )

end

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

function WFFileUtils.linkFile( sourcePath, destinationPath )

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

	if WFFileUtilsImp.linkFile then
		return WFFileUtilsImp.linkFile( sourcePath, destinationPath )
	end

end

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

-- @mark unique file name operations

--------------------------------------------------------------------------------
--[[!
	\brief Adapts a file name so that it will be unique within its containing folder.

	\param path (string) The absolute path to a file which may or may not exist.

	\return (string) If no file exists at this location, returns the same path
		as on input. If the file exists, the path is modified so that it is unique.
		If unable to construct a suitable path, will return nil.

	\warning There is a slight risk of a race condition with this API in that
		someone else could create a file with the same name in between this
		call and the operation that creates the file.
]]--

function WFFileUtils.ensureFileNameIsUnique( folderPath )

	check( '+string' )

	return WFFileUtilsImp.ensureFileNameIsUnique( folderPath )

end

--------------------------------------------------------------------------------
--[[!
	\brief Copies a file from one path to another. If a file already exists at the
		destination path, chooses a new name in the destination folder so as not to
		overwrite the existing file.

	If source and destination strings are identical, this function is a no-op by default.

	\param src (string) The absolute path to the file to copy.
	\param dest (string) The absolute path to the desired destination path.
	\param tryHardLink (boolean) Mac & iOS only: Optional flag which if true will
		cause this routine to try a hard link before making a copy.
	\param allowDuplication (boolean) If the source and destination paths are same, then
		duplicate the source file.

	\return (string) The complete path to the new file.

	Throws an error if unable to complete this operation.
]]--

function WFFileUtils.copyToUniquelyNamedFile( src, dest, tryHardLink, allowDuplication )

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

	if src == dest and not allowDuplication then
		return src
	end

	return WFFileUtilsImp.copyToUniquelyNamedFile( src, dest, tryHardLink )

end

--------------------------------------------------------------------------------
if WFPlatformInfo.isAndroid then
	--[[!
		\brief Copies a file from one path to another. If a file already exists at the
			destination path, chooses a new name in the destination folder so as not to
			overwrite the existing file.

		\param srcFD (number) File descriptor pointing to the file to copy.
		\param dest (string) The absolute path to the desired destination path.

		\return (string) The complete path to the new file.

		Throws an error if unable to complete this operation.
	]]--

	function WFFileUtils.copyToUniquelyNamedFileUsingFileDescriptor( src, dest )

		check( 'number', '+string' )

		return WFFileUtilsImp.copyToUniquelyNamedFileUsingFileDescriptor( src, dest )

	end
end
--------------------------------------------------------------------------------
--[[!
	\brief Moves a file from one path to another. If a file already exists at the
		destination path, chooses a new name at the destination so as not to
		overwrite the existing file.

	If source and destination strings are identical, this function is a no-op.

	\param source (string) The absolute path to the file to move.
	\param dest (string) The absolute path to the desired destination path.

	\return (string) The complete path to the new file.

	Throws an error if unable to complete this operation.
]]--

function WFFileUtils.moveToUniquelyNamedFile( src, dest )

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

	if src == dest then
		return src
	end

	return WFFileUtilsImp.moveToUniquelyNamedFile( src, dest )

end

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

-- @mark file attributes

--------------------------------------------------------------------------------
--[[!
	\brief Reports the size, creation data, and modification date of a file.

	\param path (string) The absolute path of the file.

	\return (table) A table with the following entries:
		<ul>
			<li>fileSize: the number of bytes </li>
			<li>fileCreationDate: the number of seconds since Jan 1, 2000.</li>
			<li>fileCreationDateTimezoneOffset</li>
			<li>fileModificationDate: the number of seconds since Jan 1, 2000.</li>
			<li>fileModificationDateTimezoneOffset</li>
		</ul>

	If the file does not exist or the OS calls to retrieve these values fails,
	returns an empty table.
]]--

function WFFileUtils.fileAttributes( path )

	check( '+string' )

	return WFFileUtilsImp.fileAttributes( path )

end

--------------------------------------------------------------------------------
--[[!
	\brief Sets the file-system date attributes of a file.

	\param path (string) The absolute path of the file.
	\param attributes (table) A table that can have entries for
		<b>fileCreationDate</b> and <b>fileModificationDate</b>.

	Throws an error if unable to complete this operation.
]]--

function WFFileUtils.setFileAttributes( path, attributes )

	check( '+string', 'table' )

	return WFFileUtilsImp.setFileAttributes( path, attributes )

end

--------------------------------------------------------------------------------
--[[!
	\brief Reports the space available and other statistics about the file system
		where a particular file is stored.

	\param path (string) The absolute path of the file.

	\return (table) A table with the following entries:
		<ul>
			<li>fileSystemSize: the total number of bytes of storage on
				this file system</li>
			<li>fileSystemFreeSize: the number of bytes on this file system
				that are currently unused</li>
			<li>fileSystemNumber: a system-dependent ID number that uniquely
				identifies this file system</li>
		</ul>

	Throws an error if unable to complete this operation.
]]--

function WFFileUtils.fileSystemAttributes( path )

	check( '+string' )

	local directoryToCheck = path

	if WFFileUtils.exists( path ) ~= 'directory' then

		directoryToCheck = KSPathUtils.parent( path )
			-- Make sure the check is done on a directory, not on a file.

		if not directoryToCheck or WFFileUtils.exists( directoryToCheck ) ~= 'directory' then
			error( string.format( 'fileSystemAttributes: Cannot determine directory for "%s"', path ) )
		end

	end

	return WFFileUtilsImp.fileSystemAttributes( directoryToCheck )

end

--------------------------------------------------------------------------------
--[[!
	\brief Reports how much space is available at a given path.

	This is a convenience wrapper for \c fileSystemAttributes.

	\param path (string) The absolute path to report on.

	\return (number) the number of bytes on this file system that are currently unused
	\return (number) the total number of bytes of storage on this file system

	Throws an error if unable to complete this operation.
]]--

function WFFileUtils.freeSpaceAtPath( path )

	check( '+string' )

	local attrs = WFFileUtils.fileSystemAttributes( path )

	local fileSystemFreeSize = attrs and attrs.fileSystemFreeSize
	local fileSystemSize = attrs and attrs.fileSystemSize

	return fileSystemFreeSize, fileSystemSize

end

--------------------------------------------------------------------------------
--[[!
	\brief Sets the "do not back up" attribute on a file.

	Only meaningful on iOS; no-op on all other platforms.

	\param path (string) The absolute path of the file.
--]]

function WFFileUtils.setDoNotBackUpFlag( path )

	check( '+string' )

	if WFFileUtilsImp.setDoNotBackUpFlag then
		return WFFileUtilsImp.setDoNotBackUpFlag( path )
	end

end

--------------------------------------------------------------------------------
--[[!
	\brief Removes the "do not back up" attribute on a file.

	Only meaningful on iOS; no-op on all other platforms.

	\param path (string) The absolute path of the file.
--]]

function WFFileUtils.removeDoNotBackUpFlag( path )

	check( '+string' )

	if WFFileUtilsImp.removeDoNotBackUpFlag then
		return WFFileUtilsImp.removeDoNotBackUpFlag( path )
	end

end


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

-- @mark Alias resolution

--------------------------------------------------------------------------------
--[[!
	\brief Resolve any aliases included in the file path.

	Only meaningful on Mac OS X; no-op on all other platforms.

	\param path (string) The absolute path of the file.

	\return (string) The fully-resolved absolute path of the file.
		On platforms other than Mac OS X, simply returns the input path.
--]]

function WFFileUtils.resolveAliases( path )

	check( '+string' )

	if WFFileUtilsImp.resolveAliases then
		return WFFileUtilsImp.resolveAliases( path )
	else
		return path
	end

end

--------------------------------------------------------------------------------
--[[!
	\brief Create an alias for a given path. Only used by Unit Tests

	Only meaningful on Mac OS X; no-op on all other platforms.

	\param srcFolder (string) The absolute path of the folder.
	\param aliasFolder (string) The path of the alias to create.
	\param aliasName (name) The name of the alias file/folder.

	\return (void)
--]]

function WFFileUtils._DEBUG_createAliasForPath( srcFolder, aliasFolder, aliasName )

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

	if WFFileUtilsImp.createAliasForPath then
		return WFFileUtilsImp.createAliasForPath( srcFolder, aliasFolder, aliasName )
	end

end

--------------------------------------------------------------------------------
--[[!
	\brief Calculates the size of all files at a given path. If the path is a file, it
		returns the file size. If the path is a directory, it should return the size of
		all files underneath that directory.

	If the path does not exist, it will return 0

	\param path (string) The path to calculate the size for.

	\return (number) The size of all files at the specified path
--]]

function WFFileUtils.calculatePathSize( path )

	check( '+string' )

	return WFFileUtilsImp.calculatePathSize( path )

end

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

local function _macVolume( path )

	local _, _, name = string.find( path, "^(/Volumes/[^/]+)" )

	return name

end

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

local function _winDrive( path )

	local _, _, name = string.find( path, "^([a-zA-Z]:)" )

	return name and string.upper( name )

end

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

local function _uncPath( path )

	local _, _, name = string.find( path, "^(\\\\[^\\]+\\)" )

	return name

end

--------------------------------------------------------------------------------
--[[!
	\brief Gets the volume for a specified path.

	\param path (string) The path to extract the volume from.

	\return (string) The volume for the specified path.
--]]

function WFFileUtils.volumeForPath( path )

	check( '+string' )

	local volume

	volume = _macVolume( path )
	volume = volume or _winDrive( path )
	volume = volume or _uncPath( path )

	volume = volume or "/"

	return volume

end

--------------------------------------------------------------------------------
--[[!
	\brief Figures out if a specified path is on an offline volume.

	\param path (string) The path to test.

	\return (boolean) True if the volume is offline, false otherwise.
--]]

function WFFileUtils.isPathOnOfflineVolume( path )

	if not path then
		return false -- no path given, not an offline volume
	end

	local volumePath = WFFileUtils.volumeForPath( path )

	if not volumePath then
		return false -- malformed path, not an offline volume
	end

	return WFFileUtils.exists( volumePath ) ~= "directory"

end

--------------------------------------------------------------------------------
--[[!
	\brief MAC ONLY: Figures out the name of the volume represented by '/'

	\return (string) The name of the volume.
--]]

function WFFileUtils.getRootVolumeName()

	if WFFileUtilsImp.getRootVolumeName then
		return WFFileUtilsImp.getRootVolumeName()
	end

	return '/'

end

--------------------------------------------------------------------------------
--[[!
	\brief Get the GUID of the volume in the specified path.

	\param path (string) A file path.

	\return
		(string|boolean) GUID of the volume.
			Returns false if it cannot be obtained.
		(?string) Mac Only: Error message, if we can't get the volume guid.
		(?string) Mac Only: Disk info, if we can't get the volume info.
--]]

function WFFileUtils.getVolumeGuidForPath( path )

	check( '+string' )

	local mountPoint = WFFileUtils.volumeForPath( path )

	-- On Windows, we need to make sure the mount point ends in '\'.
	-- volumeForPath strips this leaving something like "C:"

	if mountPoint and
		mountPoint ~= '/' and
		WFPlatformInfo.isWindows then

		mountPoint = mountPoint .. '\\'

	elseif WFPlatformInfo.isMac then

		if mountPoint == '/' then

			mountPoint = WFFileUtils.getRootVolumeName()

		end

	end

	if mountPoint and WFFileUtilsImp.getVolumeGuidForMountPoint then
		return WFFileUtilsImp.getVolumeGuidForMountPoint( mountPoint )
	end

	return mountPoint

end

--------------------------------------------------------------------------------
--[[!
	\brief Get the volume using the specified GUID.

	\param guid (string) The volume GUID.

	\return (string|boolean) The mount point of the volume. Returns false if it cannot be obtained.
		Example on Windows: C:\ or F:\
		Example on Mac: /Volumes/Foo or /
--]]

function WFFileUtils.getVolumeFromGuid( guid )

	check( '+string' )

	local volume = mapGuidToVolume[ guid ]
	if volume then
		return volume
	end

	local mountPoint, isNetworkDrive

	if WFFileUtilsImp_getVolumeMountPointFromGuid then
		mountPoint, isNetworkDrive = WFFileUtilsImp_getVolumeMountPointFromGuid( guid )
	else
		mountPoint = guid
	end

	if mountPoint then

		if WFPlatformInfo.isMac then

			if mountPoint == WFFileUtils.getRootVolumeName() then
				volume = '/'
			elseif not isNetworkDrive then
				volume = '/Volumes/' .. mountPoint
			end

		elseif WFPlatformInfo.isWindows and isNetworkDrive then

			volume = WFFileUtils.volumeForPath( mountPoint )

		end

	end

	volume = volume or mountPoint

	mapGuidToVolume[ guid ] = volume

	return volume

end

--------------------------------------------------------------------------------
--[[!
	\brief Get the volume name using the specified GUID.

	\param guid (string) The volume GUID.

	\return (string|boolean) The name of the volume. Returns false if it cannot be obtained.
		Example on Windows: "OSDisk" or "THUMB"
		Example on Mac: "Foo" or "Macintosh HD"
--]]

function WFFileUtils.getVolumeNameFromGuid( guid )

	check( '+string' )

	local volumeName

	if WFFileUtilsImp.getVolumeNameFromGuid then
		volumeName = WFFileUtilsImp.getVolumeNameFromGuid( guid )
	elseif WFFileUtilsImp_getVolumeMountPointFromGuid then
		volumeName = WFFileUtilsImp_getVolumeMountPointFromGuid( guid )
	else
		volumeName = guid
	end

	return volumeName

end

--------------------------------------------------------------------------------
--[[!
	\brief Given the volume guid, check to see if the volume is online.

	\param guid (string) The volume GUID.

	\return (boolean) Returns true if the volume is online, false otherwise.
--]]

function WFFileUtils.isVolumeGuidOnline( guid )

	check( '+string' )

	-- Force a call to platform code when checking if the volume is online
	-- by clearing the cached value which was needed for performance.

	mapGuidToVolume[ guid ] = nil

	local volumeName = WFFileUtils.getVolumeFromGuid( guid )

	return volumeName and WFFileUtils.exists( volumeName ) == "directory" or false

end

--------------------------------------------------------------------------------
--[[!
	\brief Remove the volume prefix from the beginning of an absolute path.

	\param path (string) A file path.

	\return (string) The file path without the volume prefix. For example:

	Example Mac: /Volumes/foo/bar/baz.jpg -> /bar/baz.jpg
	Example Mac: /git/foo/bar -> git/foo/bar
	Example Windows: c:\git\foo\bar -> git\foo\bar

--]]


function WFFileUtils.removeVolumeFromPath( path )

	check( 'string' )

	local volume = WFFileUtils.volumeForPath( path )
	return volume and string.sub( path, string.len( volume ) + 1, -1 )

end

--------------------------------------------------------------------------------
--[[!
	\brief Get the full path of a file given the path relative to the volume identifier,
	and the guid of the volume.

	\param relativePath (string) The path of the file, sans volume identifier.
	\param guid (string) The volume GUID.

	\return (string) The absolute path to the file.
--]]

function WFFileUtils.createFullPathFromRelativePathAndGuid( relativePath, guid )

	check( 'string', '+string' )

	local volumePath = WFFileUtils.getVolumeFromGuid( guid )

	return volumePath and KSPathUtils.child( volumePath, relativePath )

end

-----------------------------------------------------------------
--[[!
	\brief Get the full path of a file given the path relative to the catalog path,

	\param catalog (WFCatalog) The catalog

	\param relativePath (string) The relative path of the file with respect to the catalog base path


	\return (string) The full path to the file.
--]]

function WFFileUtils.createFullPathFromRelativePathAndCatalogPath( catalog, relativePath, optBinaryType )

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

	if WFPlatformInfo.isUnitTest() then
		return KSPathUtils.child( KSPathUtils.getStandardFilePath( "testdata" ), relativePath )
	end

	local catalogPath = catalog:fileSpec() and catalog:fileSpec():catalogPath()

	return KSPathUtils.child( catalogPath, relativePath )

end

--------------------------------------------------------------------------------
--[[!
	\brief MAC ONLY: Check to see if a path points to a mounted DMG.

	\param path (string) A file path.

	\return (boolean) True if the path points to a mounted DMG. False otherwise.

--]]

function WFFileUtils.isMountedDMG( path )

	check( 'string' )

	if WFFileUtilsImp.isMountedDMG then
		local volume = WFFileUtils.volumeForPath( path )
		return volume and WFFileUtilsImp.isMountedDMG( volume )
	end

	return false

end

--------------------------------------------------------------------------------
--[[!
	\brief MAC ONLY: Check to see if a volume is read only.

	\param guid (string) The volume GUID.

	\return (boolean) True if the volume is read-only. This is the case for NTFS
		formatted drives.

--]]

function WFFileUtils.isVolumeReadOnly( guid )

	check( 'string' )

	if WFFileUtilsImp.isVolumeReadOnly then
		return WFFileUtilsImp.isVolumeReadOnly( guid )
	end

	return false

end

--------------------------------------------------------------------------------
--[[!
	\brief Check to see if a directory is read only.

	\param path (string) The path to the directory.

	\return (boolean) True if the directory is read-only. False otherwise.

--]]

function WFFileUtils.isDirectoryReadOnly( path )

	check( 'string' )

	local isReadOnly = false

	local guid = WFFileUtils.getVolumeGuidForPath( path )

	if guid and WFFileUtilsImp.isVolumeReadOnly then
		isReadOnly = WFFileUtilsImp.isVolumeReadOnly( guid )
	end

	if not isReadOnly and WFFileUtilsImp.isDirectoryReadOnly then
		isReadOnly = WFFileUtilsImp.isDirectoryReadOnly( path )
	end

	return isReadOnly

end

--------------------------------------------------------------------------------
--[[!
	\brief WIN ONLY: Check to see if a path points to a CDROM

	\param path (string) A file path.

	\return (boolean) True if the path points to a CDROM. False otherwise.

--]]

function WFFileUtils.isCDROM( path )

	check( 'string' )

	if WFFileUtilsImp.isCDROM then
		local volume = WFFileUtils.volumeForPath( path )
		return volume and WFFileUtilsImp.isCDROM( volume )
	end

	return false

end

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

function WFFileUtils.resolveAliasesForPath( path )

	check( 'string' )

	if WFFileUtilsImp.resolveAliasesForPath then
		path = WFFileUtilsImp.resolveAliasesForPath( path )
	end

	return path

end

--------------------------------------------------------------------------------
--[[!
	\brief Get the contents of a file. This works with Unicode paths
	and files, which is not the case for io.open.

	\param path (string) A file path.

	\return (string) The file contents.
--]]

function WFFileUtils.getFileContents( path )

	check( 'string' )

	local contents = nil

	if WFFileUtilsImp.getFileContents then
		contents = WFFileUtilsImp.getFileContents( path )
	end

	return contents

end

--------------------------------------------------------------------------------
--[[!
	\brief Write contents to a file. This works with unicode file
		paths and content.

	\param path (string) A file path.
	\param contents (string) The contents to write to the file.

	\return (void)
--]]

function WFFileUtils.saveToFile( path, contents )

	check( 'string', 'string' )

	if WFFileUtilsImp.saveToFile then
		WFFileUtilsImp.saveToFile( path, contents )
	end

end

--------------------------------------------------------------------------------
--[[!
	\brief check to see if the file is readable.
	\param path (string) A file path.

	\return (boolean) True if the file is readable. False otherwise.
--]]

function WFFileUtils.isFileReadable( filePath )

	check( 'string' )

	local fh = io.open( filePath, "rb" )
	if( fh ) then
		fh:close()
	end

	return fh ~= nil

end

--------------------------------------------------------------------------------
--[[!
	\brief ensure a filename contains no illegal characters
		(replacing any it finds with underscores).  Assumes there is only
		a filename with no path.
		We do the same on both macOS and Windows for LrD consistency even 
		though only Windows cares about this.
		See: https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file
	\param path (string) A filename
	
	\return (string) A safe filename
--]]

function WFFileUtils.makeSafeFileName( fileName )

	check( 'string' )

	-- Replace any invalid char with _
	-- Note that windows doesn't support filenames with some
	-- reserved keywords as well but the filename has to be just the
	-- keyword (e.g. "com1") and we always add an extension so we
	-- don't run into this

	return string.gsub( fileName, "[<>:\"/\\|?*]", "_" )

end

--------------------------------------------------------------------------------
if WFPlatformInfo.isAndroid then
	--[[!
		\brief Returns read only file descriptor for the file
		\param path (string) A file path.

		\return (int) File descriptor
	--]]

	function WFFileUtils.getReadOnlyFileDescriptor( filePath )

		check( 'string' )

		return WFFileUtilsImp.getReadOnlyFileDescriptor( filePath )

	end

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

	--[[!
		\brief Returns writable file descriptor for the file
		\param path (string) A file path.

		\return (int) File descriptor
	--]]

	function WFFileUtils.getWritableFileDescriptor( filePath )

		check( 'string' )

		return WFFileUtilsImp.getWritableFileDescriptor( filePath )

	end
end

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

-- @mark Monitor File System Changes

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

--[[!

	\brief Reports files that have been updated since the specified time.

	\param path (string) The absolute path of the directory.
	\param date (string) The specified time as an ISO-8601 date string.

	\return Returns a table with files that have been updated:

		{
			< file path >,
			< file path >,
			< file path >,
			...
		}

	Note 1: this function does not report deletions.
	Note 2: this function relies on file creation and modified dates, which
		can be faked.

]]--

function WFFileUtils.getDirectoryUpdatesSince( path, date )

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

	local useUnixEpoch = WFPlatformInfo.isAndroid
	local dateAsSecondsSinceReference = KSDate.iso8601ToTimeIntervalSinceReference( date, useUnixEpoch )
		-- Convert the ISO-8601 date string to a seconds-since-reference date,
		-- since it is easier to work with in native code.

	return WFFileUtilsImp.getDirectoryUpdatesSince( path, dateAsSecondsSinceReference )

end

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

if WFConfiguration.loggers[ 'WFFileUtils' ] or logger:will_debug() or logger:will_info() then

	local KSStringUtils = require 'KSStringUtils'
	local WFCurrentThread = require 'WFCurrentThread'
	local WFDiagnosticLog = require 'WFDiagnosticLog'

	local KSDate_timeNowForAnimation = KSDate.timeNowForAnimation

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

	local function pack( ... )

		return { ... }, select( '#', ... )

	end

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

	local threadName

	local function getThreadName()

		if threadName == nil then
			threadName = WFCurrentThread.DEBUG_getThreadName() or 'main'

			local spaceAt = string.find( threadName, " " )
			if spaceAt then
				threadName = string.sub( threadName, 1, spaceAt - 1 )
			end

		end

		return threadName

	end

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

	local function descriptionFromCallStack( depthOfCaller )

		local debugInfo = debug.getinfo( depthOfCaller + 1 )
		return string.format( "%s@%s:%s", ( debugInfo.name or 'unknown' ), debugInfo.source, debugInfo.currentline )

	end

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

	local apiVolumeStats = {}

	local function accumulate( volumeName, funcName, caller, duration )

		local volStats = apiVolumeStats[ volumeName ]

		if volStats == nil then

			volStats = {
				apiCalls = {},
				apiDuration = {},
			}

			apiVolumeStats[ volumeName ] = volStats

		end

		local apiCalls = volStats.apiCalls
		local apiDuration = volStats.apiDuration

		local bucket = tostring( funcName ) .. ' / ' .. tostring( caller )
		local calls = apiCalls[ bucket ] or 0
		local dur = apiDuration[ bucket ] or 0

		calls = calls + 1
		dur = dur + duration

		apiCalls[ bucket ] = calls
		apiDuration[ bucket ] = dur

		return dur, calls
	end

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

	-- Copy function here to avoid the logging from increasing it's counts.
	local volumeForPath = WFFileUtils.volumeForPath

	local function logStats( funcName )

		-- Save the function as an upvalue so we call the original version.
		local func = WFFileUtils[ funcName ]

		return function ( ... )

			local start = KSDate_timeNowForAnimation()

			local ret, nRet = pack( func( ... ) )

			local duration = KSDate_timeNowForAnimation() - start

			local caller = descriptionFromCallStack( 2 )

			local thread = getThreadName()

			local volumeName = volumeForPath( tostring( select( 1, ... ) or "?" ) )

			local apiDuration = accumulate( volumeName, funcName, caller, duration )

			logger:infof( "total = %s, funcName %s, caller %s, volumeName %s, thread %s, duration %s", apiDuration, funcName, caller, volumeName, thread, duration )

			return unpack( ret, 1, nRet )

		end

	end

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

	WFDiagnosticLog.addLogSectionFunc( function()

		local output = {
		}

		local function addln( s )
			output[ #output + 1 ] = s or ""
		end

		for volName, v in pairs( apiVolumeStats ) do

			local apiDuration = v.apiDuration
			local apiCalls = v.apiCalls

			addln( string.format( "Stats for Volume: %s", volName ) )
			addln()
			addln( "   Duration   Call Count      Average  Call / Caller" )
			addln( "-----------  -----------  -----------  --------------------------" )


			local sortedStats = {}
			local n = 0

			local totalCount = 0
			local totalDuration = 0

			for k, duration in pairs( apiDuration ) do

				n = n + 1

				local count = apiCalls[ k ] or 1
				totalDuration = totalDuration + duration or 0
				totalCount = totalCount + count

				sortedStats[ n ] = {
					duration = duration,
					callCount = count,
					average = duration / count,
					description = k,
				}

			end

			table.insert( sortedStats, 1, {
				duration = totalDuration,
				callCount = totalCount,
				average = totalDuration / totalCount,
				description = 'Total'
			} )

			table.sort( sortedStats, function( a, b )
				return a.duration > b.duration
			end )

			for i = 1, #sortedStats do

				local stat = sortedStats[ i ]

				addln( string.format( "%11.3f  %11d  %11.3f  %s",
					stat.duration,
					stat.callCount,
					stat.average,
					stat.description ) )

			end

			addln()

		end

		local htmlReport = string.format( "<pre>%s</pre>", KSStringUtils.escapeHtml( table.concat( output, "\n" ) ) )

		WFDiagnosticLog.addSectionToLog {

			id = 'WFFileUtils-' .. getThreadName(),
			name = "WFFileUtils-" .. getThreadName(),
			weight = -999,

			body = htmlReport,

		}

	end )

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

	WFFileUtils.exists = logStats( 'exists' )
	WFFileUtils.empty = logStats( 'empty' )
	WFFileUtils.copy = logStats( 'copy' )
	WFFileUtils.move = logStats( 'move' )
	WFFileUtils.delete = logStats( 'delete' )
	WFFileUtils.deleteFile = logStats( 'deleteFile' )
	WFFileUtils.moveToTrash = logStats( 'moveToTrash' )
	WFFileUtils.mkdir = logStats( 'mkdir' )
	WFFileUtils.doForAuthorizedPath = logStats( 'doForAuthorizedPath' )
	WFFileUtils.linkFile = logStats( 'linkFile' )
	WFFileUtils.ensureFileNameIsUnique = logStats( 'ensureFileNameIsUnique' )
	WFFileUtils.copyToUniquelyNamedFile = logStats( 'copyToUniquelyNamedFile' )
	WFFileUtils.moveToUniquelyNamedFile = logStats( 'moveToUniquelyNamedFile' )
	WFFileUtils.fileAttributes = logStats( 'fileAttributes' )
	WFFileUtils.setFileAttributes = logStats( 'setFileAttributes' )
	WFFileUtils.fileSystemAttributes = logStats( 'fileSystemAttributes' )
	WFFileUtils.freeSpaceAtPath = logStats( 'freeSpaceAtPath' )
	WFFileUtils.setDoNotBackUpFlag = logStats( 'setDoNotBackUpFlag' )
	WFFileUtils.removeDoNotBackUpFlag = logStats( 'removeDoNotBackUpFlag' )
	WFFileUtils.resolveAliases = logStats( 'resolveAliases' )
	WFFileUtils._DEBUG_createAliasForPath = logStats ( '_DEBUG_createAliasForPath' )
	WFFileUtils.calculatePathSize = logStats( 'calculatePathSize' )
	WFFileUtils.volumeForPath = logStats( 'volumeForPath' )
	WFFileUtils.isPathOnOfflineVolume = logStats( 'isPathOnOfflineVolume' )
	WFFileUtils.getRootVolumeName = logStats( 'getRootVolumeName' )
	WFFileUtils.getVolumeGuidForPath = logStats( 'getVolumeGuidForPath' )
	WFFileUtils.getVolumeFromGuid = logStats( 'getVolumeFromGuid' )
	WFFileUtils.getVolumeNameFromGuid = logStats( 'getVolumeNameFromGuid' )
	WFFileUtils.isVolumeGuidOnline = logStats( 'isVolumeGuidOnline' )
	WFFileUtils.removeVolumeFromPath = logStats( 'removeVolumeFromPath' )
	WFFileUtils.createFullPathFromRelativePathAndGuid = logStats( 'createFullPathFromRelativePathAndGuid' )
	WFFileUtils.createFullPathFromRelativePathAndCatalogPath = logStats( 'createFullPathFromRelativePathAndCatalogPath' )
	WFFileUtils.isMountedDMG = logStats( 'isMountedDMG' )
	WFFileUtils.isVolumeReadOnly = logStats( 'isVolumeReadOnly' )
	WFFileUtils.isDirectoryReadOnly = logStats( 'isDirectoryReadOnly' )
	WFFileUtils.isCDROM = logStats( 'isCDROM' )
	WFFileUtils.resolveAliasesForPath = logStats( 'resolveAliasesForPath' )
	WFFileUtils.getFileContents = logStats( 'getFileContents' )
	WFFileUtils.saveToFile = logStats( 'saveToFile' )
	WFFileUtils.makeSafeFileName = logStats( 'makeSafeFileName' )

end -- logger.will_debug

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

return WFFileUtils
