import Foundation
#if !COCOAPODS
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.promise().asStandardOutput(encoding: .utf8).then { str in
             print(str)
         }
     */
    public func promise() -> ProcessPromise {
        standardOutput = Pipe()
        standardError = Pipe()

        launch()

        let (p, fulfill, reject) = ProcessPromise.pending()
        let promise = p as! ProcessPromise

        promise.task = self

        waldo.async {
            self.waitUntilExit()

            if self.terminationReason == .exit && self.terminationStatus == 0 {
                fulfill(())
            } else {
                reject(Error(.execution, promise, self))
            }

            promise.task = nil
        }

        return promise
    }

    /**
     The error generated by PromiseKit’s `Process` extension
     */
    public struct Error: Swift.Error, CustomStringConvertible {
        public let exitStatus: Int
        public let stdout: Data
        public let stderr: Data
        public let args: [String]
        public let code: Code
        public let cmd: String

        init(_ code: Code, _ promise: ProcessPromise, _ task: Process) {
            stdout = promise.stdout
            stderr = promise.stderr
            exitStatus = Int(task.terminationStatus)
            cmd = task.launchPath ?? ""
            args = task.arguments ?? []
            self.code = code
        }

        /// The type of `Process` error
        public enum Code {
            /// The data could not be converted to a UTF8 String
            case encoding
            /// The task execution failed
            case execution
        }

        /// A textual representation of the error
        public var description: String {
            switch code {
            case .encoding:
                return "Could not decode command output into string."
            case .execution:
                let str = ([cmd] + args).joined(separator: " ")
                return "Failed executing: `\(str)`."
            }
        }
    }
}

final public class ProcessPromise: Promise<Void> {
    fileprivate var task: Process!

    fileprivate var stdout: Data {
        return (task.standardOutput! as! Pipe).fileHandleForReading.readDataToEndOfFile()
    }

    fileprivate var stderr: Data {
        return (task.standardError! as! Pipe).fileHandleForReading.readDataToEndOfFile()
    }

    public func asStandardOutput() -> Promise<Data> {
        return then(on: zalgo) { _ in self.stdout }
    }

    public func asStandardError() -> Promise<Data> {
        return then(on: zalgo) { _ in self.stderr }
    }

    public func asStandardPair() -> Promise<(Data, Data)> {
        return then(on: zalgo) { _ in (self.stderr, self.stdout) }
    }

    private func decode(_ encoding: String.Encoding, _ data: Data) throws -> String {
        guard let str = String(bytes: data, encoding: encoding) else {
            throw Process.Error(.encoding, self, self.task)
        }
        return str
    }

    public func asStandardPair(encoding: String.Encoding) -> Promise<(String, String)> {
        return then(on: zalgo) { _ in
            (try self.decode(encoding, self.stdout), try self.decode(encoding, self.stderr))
        }
    }

    public func asStandardOutput(encoding: String.Encoding) -> Promise<String> {
        return then(on: zalgo) { _ in try self.decode(encoding, self.stdout) }
    }

    public func asStandardError(encoding: String.Encoding) -> Promise<String> {
        return then(on: zalgo) { _ in try self.decode(encoding, self.stderr) }
    }
}

#endif
