import Foundation
#if !PMKCocoaPods
import PromiseKit
#endif

#if os(macOS)

/**
 To import the `Process` category:

    use_frameworks!
    pod "PromiseKit/Foundation"

 Or `Process` is one of the categories imported by the umbrella pod:

    use_frameworks!
    pod "PromiseKit"
 
 And then in your sources:

    import PromiseKit
 */
extension Process {
    /**
     Launches the receiver and resolves when it exits.
     
         let proc = Process()
         proc.launchPath = "/bin/ls"
         proc.arguments = ["/bin"]
         proc.launch(.promise).compactMap { std in
             String(data: std.out.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)
         }.then { stdout in
             print(str)
         }
     */
    public func launch(_: PMKNamespacer) -> Promise<(out: Pipe, err: Pipe)> {
        let (stdout, stderr) = (Pipe(), Pipe())

        do {
            standardOutput = stdout
            standardError = stderr

            #if swift(>=4.0)
                if #available(OSX 10.13, *) {
                    try run()
                } else if let path = launchPath, FileManager.default.isExecutableFile(atPath: path) {
                    launch()
                } else {
                    throw PMKError.notExecutable(launchPath)
                }
            #else
                guard let path = launchPath, FileManager.default.isExecutableFile(atPath: path) else {
                    throw PMKError.notExecutable(launchPath)
                }
                launch()
            #endif
        } catch {
            return Promise(error: error)
        }


        var q: DispatchQueue {
            if #available(macOS 10.10, iOS 8.0, tvOS 9.0, watchOS 2.0, *) {
                return DispatchQueue.global(qos: .default)
            } else {
                return DispatchQueue.global(priority: .default)
            }
        }

        return Promise { seal in
            q.async {
                self.waitUntilExit()

                guard self.terminationReason == .exit, self.terminationStatus == 0 else {
                    let stdoutData = try? self.readDataFromPipe(stdout)
                    let stderrData = try? self.readDataFromPipe(stderr)

                    let stdoutString = stdoutData.flatMap { (data: Data) -> String? in String(data: data, encoding: .utf8) }
                    let stderrString = stderrData.flatMap { (data: Data) -> String? in String(data: data, encoding: .utf8) }

                    return seal.reject(PMKError.execution(process: self, standardOutput: stdoutString, standardError: stderrString))
                }
                seal.fulfill((stdout, stderr))
            }
        }
    }

    private func readDataFromPipe(_ pipe: Pipe) throws -> Data {
        let handle = pipe.fileHandleForReading
        defer { handle.closeFile() }

        // Someday, NSFileHandle will probably be updated with throwing equivalents to its read and write methods,
        // as NSTask has, to avoid raising exceptions and crashing the app.
        // Unfortunately that day has not yet come, so use the underlying BSD calls for now.

        let fd = handle.fileDescriptor

        let bufsize = 1024 * 8
        let buf = UnsafeMutablePointer<UInt8>.allocate(capacity: bufsize)

        #if swift(>=4.1)
            defer { buf.deallocate() }
        #else
            defer { buf.deallocate(capacity: bufsize) }
        #endif

        var data = Data()

        while true {
            let bytesRead = read(fd, buf, bufsize)

            if bytesRead == 0 {
                break
            }

            if bytesRead < 0 {
                throw POSIXError.Code(rawValue: errno).map { POSIXError($0) } ?? CocoaError(.fileReadUnknown)
            }

            data.append(buf, count: bytesRead)
        }

        return data
    }

    /**
     The error generated by PromiseKit’s `Process` extension
     */
    public enum PMKError {
        /// NOT AVAILABLE ON 10.13 and above because Apple provide this error handling themselves
        case notExecutable(String?)
        case execution(process: Process, standardOutput: String?, standardError: String?)
    }
}


extension Process.PMKError: LocalizedError {
    public var errorDescription: String? {
        switch self {
        case .notExecutable(let path?):
            return "File not executable: \(path)"
        case .notExecutable(nil):
            return "No launch path specified"
        case .execution(process: let task, standardOutput: _, standardError: _):
            return "Failed executing: `\(task)` (\(task.terminationStatus))."
        }
    }
}

public extension Promise where T == (out: Pipe, err: Pipe) {
    func print() -> Promise<T> {
        return tap { result in
            switch result {
            case .fulfilled(let raw):
                let stdout = String(data: raw.out.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)
                let stderr = String(data: raw.err.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)
                Swift.print("stdout: `\(stdout ?? "")`")
                Swift.print("stderr: `\(stderr ?? "")`")
            case .rejected(let err):
                Swift.print(err)
            }
        }
    }
}

extension Process {
    /// Provided because Foundation’s is USELESS
    open override var description: String {
        let launchPath = self.launchPath ?? "$0"
        var args = [launchPath]
        arguments.flatMap{ args += $0 }
        return args.map { arg in
            let contains: Bool
          #if swift(>=3.2)
            contains = arg.contains(" ")
          #else
            contains = arg.characters.contains(" ")
          #endif
            if contains {
                return "\"\(arg)\""
            } else if arg == "" {
                return "\"\""
            } else {
                return arg
            }
        }.joined(separator: " ")
    }
}

#endif
