//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2023 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

#if FOUNDATION_FRAMEWORK
internal import containermanager
internal import _ForSwiftFoundation
internal import os
#endif

#if canImport(Darwin)
import Darwin
#elseif os(Android)
import Android
import unistd
#elseif canImport(Glibc)
import Glibc
#elseif canImport(Musl)
import Musl
#elseif os(Windows)
import CRT
import WinSDK
#elseif os(WASI)
import WASILibc
#endif

internal import _FoundationCShims

#if FOUNDATION_FRAMEWORK
func _LogSpecialFolderRecreation(_ fileManager: FileManager, _ path: String) {
    if UserDefaults.standard.bool(forKey: "NSLogSpecialFolderRecreation") && !fileManager.fileExists(atPath: path) {
        Logger().info("*** Application: \(Bundle.main.bundleIdentifier ?? "(null)") just recreated special folder: \(path)")
    }
}
#endif

extension _FileManagerImpl {
    var homeDirectoryForCurrentUser: URL {
        URL(filePath: String.homeDirectoryPath(), directoryHint: .isDirectory)
    }
    
    func homeDirectory(forUser userName: String?) -> URL? {
        URL(filePath:  String.homeDirectoryPath(forUser: userName), directoryHint: .isDirectory)
    }
    
    var temporaryDirectory: URL {
        URL(filePath: String.temporaryDirectoryPath, directoryHint: .isDirectory)
    }
    
    func url(
        for directory: FileManager.SearchPathDirectory,
        in domain: FileManager.SearchPathDomainMask,
        appropriateFor url: URL?,
        create shouldCreate: Bool
    ) throws -> URL {
        #if FOUNDATION_FRAMEWORK
        // TODO: Support correct trash/replacement locations in swift-foundation
        #if os(macOS) || os(iOS)
        if let url, directory == .trashDirectory {
            return try fileManager._URLForTrashingItem(at: url, create: shouldCreate)
        }
        #endif
        if let url, domain == .userDomainMask, directory == .itemReplacementDirectory {
            // The only place we need to do this is for certain operations, namely the replacing item API.
            return try fileManager._URLForReplacingItem(at: url)
        }
        var domain = domain
        if domain == .systemDomainMask {
            domain = ._partitionedSystemDomainMask
        }
        let lastElement = domain == ._partitionedSystemDomainMask
        #else
        let lastElement = false
        #endif
        let urls = Array(_SearchPathURLs(for: directory, in: domain, expandTilde: true))
        guard let url = lastElement ? urls.last : urls.first else {
            throw CocoaError(.fileReadUnknown)
        }
        
        if shouldCreate {
            #if FOUNDATION_FRAMEWORK
            _LogSpecialFolderRecreation(fileManager, url.path)
            #endif
            var isUserDomain = domain == .userDomainMask
            #if os(macOS) && FOUNDATION_FRAMEWORK
            isUserDomain = isUserDomain || domain == ._sharedUserDomainMask
            #endif
            var attrDictionary: [FileAttributeKey : Any] = [:]
            if isUserDomain {
                attrDictionary[.posixPermissions] = 0o700
            } else {
                #if FOUNDATION_FRAMEWORK
                if domain == ._partitionedSystemDomainMask {
                    attrDictionary[.posixPermissions] = 0o755
                    attrDictionary[.ownerAccountID] = 0 // root
                    attrDictionary[.groupOwnerAccountID] = 80 // admin
                }
                #endif
            }
            try fileManager.createDirectory(at: url, withIntermediateDirectories: true, attributes: attrDictionary)
        }
        return url
    }
    
    func urls(
        for directory: FileManager.SearchPathDirectory,
        in domainMask: FileManager.SearchPathDomainMask
    ) -> [URL] {
        Array(_SearchPathURLs(for: directory, in: domainMask, expandTilde: true))
    }
    
    #if FOUNDATION_FRAMEWORK
    func containerURL(forSecurityApplicationGroupIdentifier groupIdentifier: String) -> URL? {
        groupIdentifier.withCString {
            guard let path = container_create_or_lookup_app_group_path_by_app_group_identifier($0, nil)  else {
                return nil
            }
            
            defer { path.deallocate() }
            return URL(fileURLWithFileSystemRepresentation: path, isDirectory: true, relativeTo: nil)
        }
    }
#endif
    
    func contentsOfDirectory(atPath path: String) throws -> [String] {
        #if os(macOS)
        // CFURLEnumerator/CarbonCore does not operate on /dev paths
        if !path.standardizingPath.starts(with: "/dev") {
            guard fileManager.fileExists(atPath: path) else {
                throw CocoaError.errorWithFilePath(path, osStatus: -43 /*fnfErr*/, reading: true, variant: "Folder")
            }
            
            #if FOUNDATION_FRAMEWORK
            // Use CFURLEnumerator in Foundation framework, otherwise fallback to POSIX sequence below
            var err: NSError?
            guard let result = _NSDirectoryContentsFromCFURLEnumeratorError(URL(fileURLWithPath: path, isDirectory: true), nil, 0, true, &err) else {
                throw err!
            }
            return result
            #endif
        }
        #endif
        var result: [String] = []
#if os(Windows)
        let iterator = _Win32DirectoryContentsSequence(path: path, appendSlashForDirectory: false).makeIterator()
#else
        let iterator = _POSIXDirectoryContentsSequence(path: path, appendSlashForDirectory: false).makeIterator()
#endif
        if let error = iterator.error {
            throw error
        } else {
            while let item = iterator.next() {
                result.append(item.fileName)
            }
        }
        return result
    }
    
    func subpathsOfDirectory(atPath path: String) throws -> [String] {
#if os(Windows)
        try path.withNTPathRepresentation {
            var faAttributes: WIN32_FILE_ATTRIBUTE_DATA = .init()
            guard GetFileAttributesExW($0, GetFileExInfoStandard, &faAttributes) else {
                throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: true)
            }
        }

        var results: [String] = []
        for item in _Win32DirectoryContentsSequence(path: path, appendSlashForDirectory: true) {
            results.append(item.fileName)
            if item.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY == FILE_ATTRIBUTE_DIRECTORY &&
                item.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT != FILE_ATTRIBUTE_REPARSE_POINT {

                var pwszSubPath: PWSTR? = nil
                let hr = PathAllocCombine(path, item.fileName, PATHCCH_ALLOW_LONG_PATHS, &pwszSubPath)
                guard hr == S_OK else {
                    throw CocoaError.errorWithFilePath(path, win32: WIN32_FROM_HRESULT(hr), reading: true)
                }
                defer { LocalFree(pwszSubPath) }

                results.append(contentsOf: try subpathsOfDirectory(atPath: String(decodingCString: pwszSubPath!, as: UTF16.self)).map {
                    var pwszFullPath: PWSTR? = nil
                    _ = PathAllocCombine(item.fileName, $0, PATHCCH_ALLOW_LONG_PATHS, &pwszFullPath)
                    defer { LocalFree(pwszFullPath) }
                    return String(decodingCString: pwszFullPath!, as: UTF16.self).standardizingPath.replacing("\\", with: "/")
                 })
            }
        }
        return results
#else
        return try path.withFileSystemRepresentation { fileSystemRep in
            guard let fileSystemRep else {
                throw CocoaError.errorWithFilePath(.fileNoSuchFile, path)
            }
            
            let subpaths = _FTSSequence(fileSystemRep, FTS_PHYSICAL | FTS_NOCHDIR | FTS_NOSTAT).subpaths
            var realFirstPath: String?
            
            var results: [String] = []
            for item in subpaths {
                var subpath: String
                switch item {
                case .error(let errNum, let p):
                    throw CocoaError.errorWithFilePath(p, errno: errNum, reading: true)
                case .entry(let path):
                    subpath = path
                }
                
                guard let realFirstPath else {
                    realFirstPath = subpath
                    continue
                }
                
                let trueSubpath = subpath.trimmingPrefix(realFirstPath)
                if trueSubpath.first == "/" {
                    results.append(String(trueSubpath.dropFirst()))
                } else if !trueSubpath.isEmpty {
                    results.append(String(trueSubpath))
                }
            }
            return results
        }
#endif
    }

    func createDirectory(
        at url: URL,
        withIntermediateDirectories createIntermediates: Bool,
        attributes: [FileAttributeKey : Any]? = nil
    ) throws {
        guard url.isFileURL else {
            throw CocoaError.errorWithFilePath(.fileWriteUnsupportedScheme, url)
        }
        
        let path = url.path
        guard !path.isEmpty else {
            throw CocoaError.errorWithFilePath(.fileNoSuchFile, url)
        }
        
        try fileManager.createDirectory(atPath: path, withIntermediateDirectories: createIntermediates, attributes: attributes)
    }
    
    func createDirectory(
        atPath path: String,
        withIntermediateDirectories createIntermediates: Bool,
        attributes: [FileAttributeKey : Any]? = nil
    ) throws {
#if os(Windows)
        try path.withNTPathRepresentation { pwszPath in
            if createIntermediates {
                var isDirectory: Bool = false
                if fileManager.fileExists(atPath: path, isDirectory: &isDirectory) {
                    guard isDirectory else {
                        throw CocoaError.errorWithFilePath(path, win32: ERROR_FILE_EXISTS, reading: false)
                    }
                    return
                }

                let parent = path.deletingLastPathComponent()
                if !parent.isEmpty {
                    try createDirectory(atPath: parent, withIntermediateDirectories: true, attributes: attributes)
                }
            }

            var saAttributes: SECURITY_ATTRIBUTES =
                SECURITY_ATTRIBUTES(nLength: DWORD(MemoryLayout<SECURITY_ATTRIBUTES>.size),
                                    lpSecurityDescriptor: nil,
                                    bInheritHandle: false)
            guard CreateDirectoryW(pwszPath, &saAttributes) else {
                throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: false)
            }
            if let attributes {
                try? fileManager.setAttributes(attributes, ofItemAtPath: path)
            }
        }
#else
        try fileManager.withFileSystemRepresentation(for: path) { pathPtr in
            guard let pathPtr else {
                throw CocoaError.errorWithFilePath(.fileWriteUnknown, path)
            }
            
            guard createIntermediates else {
                guard mkdir(pathPtr, 0o777) == 0 else {
                    throw CocoaError.errorWithFilePath(path, errno: errno, reading: false)
                }
                if let attributes {
                    try? fileManager.setAttributes(attributes, ofItemAtPath: path)
                }
                return
            }
            
            #if FOUNDATION_FRAMEWORK
            var firstDirectoryPtr: UnsafePointer<CChar>?
            defer { firstDirectoryPtr?.deallocate() }
            let result = _mkpath_np(pathPtr, S_IRWXU | S_IRWXG | S_IRWXO, &firstDirectoryPtr)
            
            guard result == 0 else {
                guard result != EEXIST else { return }
                var errNum = result
                var errPath = path
                if result == ENOTDIR {
                    // _mkpath_np reports ENOTDIR when any component in the path is a regular file. We need to do two things to ensure binary compatibility: 1) find that file -- we have to report it in the error, and 2) special-case the last component. For whatever reason, we've always reported EEXIST for this case. This requires some extra stat'ing.
                    var currentDirectory = path
                    var isLastComponent = true
                    // This shouldn't happen unless there are file system races going on, but stop iterating when we reach "/".
                    while currentDirectory.count > 1 {
                        if fileManager.fileExists(atPath: currentDirectory) {
                            errPath = currentDirectory
                            if isLastComponent {
                                errNum = EEXIST
                            }
                            break
                        }
                        currentDirectory = currentDirectory.deletingLastPathComponent()
                        isLastComponent = false
                    }
                }
                throw CocoaError.errorWithFilePath(errPath, errno: errNum, reading: false)
            }
            
            guard let attributes else {
                return // Nothing left to do
            }
            
            // The directory was successfully created. To keep binary compatibility, we need to post-process the newly created directories and set attributes.
            // We're relying on the knowledge that _mkpath_np does not change any of the parent path components of firstDirectory. Otherwise, I think we'd have to canonicalize paths or check for IDs, which would probably require more file system calls than is worthwhile.
            var currentDirectory = firstDirectoryPtr.flatMap(String.init(cString:)) ?? path
            
            // Start with the first newly created directory.
            try? fileManager.setAttributes(attributes, ofItemAtPath: currentDirectory)// Not returning error to preserve binary compatibility.
            
            // Now append each subsequent path component.
            let fullComponents = path.pathComponents
            let currentComponents = currentDirectory.pathComponents
            for component in fullComponents[currentComponents.count...] {
                currentDirectory = currentDirectory.appendingPathComponent(component)
                try? fileManager.setAttributes(attributes, ofItemAtPath: currentDirectory) // Not returning error to preserve binary compatibility.
            }
            #else
            func _create(path: String, leafFile: Bool = true) throws {
                var isDir = false
                guard !fileManager.fileExists(atPath: path, isDirectory: &isDir) else {
                    if !isDir && leafFile {
                        throw CocoaError.errorWithFilePath(path, errno: EEXIST, reading: false)
                    }
                    return
                }
                let parent = path.deletingLastPathComponent()
                if !parent.isEmpty {
                    try _create(path: parent, leafFile: false)
                }
                try fileManager.withFileSystemRepresentation(for: path) { pathFsRep in
                    guard let pathFsRep else {
                        throw CocoaError.errorWithFilePath(.fileWriteInvalidFileName, path)
                    }
                    guard mkdir(pathFsRep, 0o777) == 0 else {
                        let posixErrno = errno
                        if posixErrno == EEXIST && fileManager.fileExists(atPath: path, isDirectory: &isDir) && isDir {
                            // Continue; if there is an existing file and it is a directory, that is still a success.
                            // There can be an existing file if another thread or process concurrently creates the
                            // same file.
                            return
                        } else {
                            throw CocoaError.errorWithFilePath(path, errno: posixErrno, reading: false)
                        }
                    }
                    if let attr = attributes {
                        try? fileManager.setAttributes(attr, ofItemAtPath: path)
                    }
                }
            }
            try _create(path: path)
            #endif
        }
#endif
    }
    
#if FOUNDATION_FRAMEWORK
    func getRelationship(
        _ outRelationship: UnsafeMutablePointer<FileManager.URLRelationship>,
        ofDirectoryAt directoryURL: URL,
        toItemAt otherURL: URL
    ) throws {
        // Get url's resource identifier, volume identifier, and make sure it is a directory
        let dirValues = try directoryURL.resourceValues(forKeys: [.fileResourceIdentifierKey, .volumeIdentifierKey, .isDirectoryKey])
        
        guard let isDirectory = dirValues.isDirectory, isDirectory else {
            outRelationship.pointee = .other
            return
        }
        
        // Get other's resource identifier and make sure it is not the same resource as otherURL
        let otherValues = try otherURL.resourceValues(forKeys: [.fileIdentifierKey, .fileResourceIdentifierKey, .volumeIdentifierKey])
        guard !otherValues.fileResourceIdentifier!.isEqual(dirValues.fileResourceIdentifier!) else {
            outRelationship.pointee = .same
            return
        }
        
        guard otherValues.volumeIdentifier!.isEqual(dirValues.volumeIdentifier!) else {
            outRelationship.pointee = .other
            return
        }
        
        // Start looking through the parent chain up to the volume root for a parent that is equal to 'url'. Stop when the current URL reaches the volume root
        var currentURL = otherURL
        while try !currentURL.resourceValues(forKeys: [.isVolumeKey]).isVolume! {
            // Get url's parentURL
            let parentURL = try currentURL.resourceValues(forKeys: [.parentDirectoryURLKey]).parentDirectory!
            
            let parentResourceID = try parentURL.resourceValues(forKeys: [.fileResourceIdentifierKey]).fileResourceIdentifier!
            
            if parentResourceID.isEqual(dirValues.fileResourceIdentifier!) {
                outRelationship.pointee = .contains
                return
            }
            
            currentURL = parentURL
        }
        
        outRelationship.pointee = .other
        return
    }
    
    func getRelationship(
        _ outRelationship: UnsafeMutablePointer<FileManager.URLRelationship>,
        of directory: FileManager.SearchPathDirectory,
        in domainMask: FileManager.SearchPathDomainMask,
        toItemAt url: URL
    ) throws {
        // Figure out the standard directory, then call the other API
        let directoryURL = try fileManager.url(
            for: directory,
            in: domainMask,
            appropriateFor: domainMask.isEmpty ? url : nil,
            create: false)
        return try fileManager.getRelationship(
            outRelationship,
            ofDirectoryAt: directoryURL,
            toItemAt: url)
    }
#endif
    
    func changeCurrentDirectoryPath(_ path: String) -> Bool {
#if os(Windows)
        return (try? path.withNTPathRepresentation {
            SetCurrentDirectoryW($0)
        }) ?? false
#else
        fileManager.withFileSystemRepresentation(for: path) { rep in
            guard let rep else { return false }
            return chdir(rep) == 0
        }
#endif
    }
    
    var currentDirectoryPath: String? {
#if os(Windows)
        let dwLength: DWORD = GetCurrentDirectoryW(0, nil)
        return withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwLength)) {
            if GetCurrentDirectoryW(dwLength, $0.baseAddress) == dwLength - 1 {
                return String(decodingCString: $0.baseAddress!, as: UTF16.self)
            }
            return nil
        }
#else
        withUnsafeTemporaryAllocation(of: CChar.self, capacity: FileManager.MAX_PATH_SIZE) { buffer in
            guard getcwd(buffer.baseAddress!, FileManager.MAX_PATH_SIZE) != nil else {
                return nil
            }
            return fileManager.string(withFileSystemRepresentation: buffer.baseAddress!, length: strlen(buffer.baseAddress!))
        }
#endif
    }
}
